Skip to content

Commit fd1cab0

Browse files
btutSiedlerchrcalixtus
authored
Implement an interface to import PDF metadata from multiple sources (XMP, Grobid, ...) (#7929)
Co-authored-by: Christoph <[email protected]> Co-authored-by: Carl Christian Snethlage <[email protected]>
1 parent 8720fd2 commit fd1cab0

17 files changed

+924
-69
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve
1515
- We added a fulltext search feature. [#2838](https://github.com/JabRef/jabref/pull/2838)
1616
- We improved the deduction of bib-entries from imported fulltext pdfs. [#7947](https://github.com/JabRef/jabref/pull/7947)
1717
- We added unprotect_terms to the list of bracketed pattern modifiers [#7826](https://github.com/JabRef/jabref/pull/7960)
18+
- We added a dialog that allows to parse metadata from linked pdfs. [#7929](https://github.com/JabRef/jabref/pull/7929)
1819
- We added an icon picker in group edit dialog. [#6142](https://github.com/JabRef/jabref/issues/6142)
1920
- We added a preference to Opt-In to JabRef's online metadata extraction service (Grobid) usage. [8002](https://github.com/JabRef/jabref/pull/8002)
2021

src/main/java/org/jabref/gui/externalfiles/DownloadFullTextAction.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,7 @@ private void addLinkedFileFromURL(BibDatabaseContext databaseContext, URL url, B
144144
databaseContext,
145145
Globals.TASK_EXECUTOR,
146146
dialogService,
147-
preferences.getXmpPreferences(),
148-
preferences.getFilePreferences(),
147+
preferences,
149148
ExternalFileTypes.getInstance());
150149

151150
onlineFile.download();

src/main/java/org/jabref/gui/fieldeditors/LinkedFileViewModel.java

+61-26
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.util.List;
1010
import java.util.Optional;
1111
import java.util.function.BiPredicate;
12+
import java.util.function.Supplier;
1213

1314
import javax.net.ssl.HostnameVerifier;
1415
import javax.net.ssl.HttpsURLConnection;
@@ -35,23 +36,30 @@
3536
import org.jabref.gui.icon.IconTheme;
3637
import org.jabref.gui.icon.JabRefIcon;
3738
import org.jabref.gui.linkedfile.LinkedFileEditDialogView;
39+
import org.jabref.gui.mergeentries.MultiMergeEntriesView;
3840
import org.jabref.gui.util.BackgroundTask;
3941
import org.jabref.gui.util.ControlHelper;
4042
import org.jabref.gui.util.TaskExecutor;
4143
import org.jabref.logic.externalfiles.LinkedFileHandler;
44+
import org.jabref.logic.importer.Importer;
45+
import org.jabref.logic.importer.ParserResult;
46+
import org.jabref.logic.importer.fileformat.PdfContentImporter;
47+
import org.jabref.logic.importer.fileformat.PdfEmbeddedBibFileImporter;
48+
import org.jabref.logic.importer.fileformat.PdfGrobidImporter;
49+
import org.jabref.logic.importer.fileformat.PdfVerbatimBibTextImporter;
50+
import org.jabref.logic.importer.fileformat.PdfXmpImporter;
4251
import org.jabref.logic.l10n.Localization;
4352
import org.jabref.logic.net.URLDownload;
4453
import org.jabref.logic.util.io.FileNameUniqueness;
4554
import org.jabref.logic.util.io.FileUtil;
46-
import org.jabref.logic.xmp.XmpPreferences;
4755
import org.jabref.logic.xmp.XmpUtilWriter;
4856
import org.jabref.model.database.BibDatabaseContext;
4957
import org.jabref.model.entry.BibEntry;
5058
import org.jabref.model.entry.LinkedFile;
5159
import org.jabref.model.strings.StringUtil;
5260
import org.jabref.model.util.FileHelper;
5361
import org.jabref.model.util.OptionalUtil;
54-
import org.jabref.preferences.FilePreferences;
62+
import org.jabref.preferences.PreferencesService;
5563

5664
import de.saxsys.mvvmfx.utils.validation.FunctionBasedValidator;
5765
import de.saxsys.mvvmfx.utils.validation.ValidationMessage;
@@ -69,12 +77,11 @@ public class LinkedFileViewModel extends AbstractViewModel {
6977
private final DoubleProperty downloadProgress = new SimpleDoubleProperty(-1);
7078
private final BooleanProperty downloadOngoing = new SimpleBooleanProperty(false);
7179
private final BooleanProperty isAutomaticallyFound = new SimpleBooleanProperty(false);
72-
private final BooleanProperty canWriteXMPMetadata = new SimpleBooleanProperty(false);
80+
private final BooleanProperty isOfflinePdf = new SimpleBooleanProperty(false);
7381
private final DialogService dialogService;
7482
private final BibEntry entry;
7583
private final TaskExecutor taskExecutor;
76-
private final FilePreferences filePreferences;
77-
private final XmpPreferences xmpPreferences;
84+
private final PreferencesService preferences;
7885
private final LinkedFileHandler linkedFileHandler;
7986
private final ExternalFileTypes externalFileTypes;
8087

@@ -85,38 +92,36 @@ public LinkedFileViewModel(LinkedFile linkedFile,
8592
BibDatabaseContext databaseContext,
8693
TaskExecutor taskExecutor,
8794
DialogService dialogService,
88-
XmpPreferences xmpPreferences,
89-
FilePreferences filePreferences,
95+
PreferencesService preferences,
9096
ExternalFileTypes externalFileTypes) {
9197

9298
this.linkedFile = linkedFile;
93-
this.filePreferences = filePreferences;
94-
this.linkedFileHandler = new LinkedFileHandler(linkedFile, entry, databaseContext, filePreferences);
99+
this.preferences = preferences;
100+
this.linkedFileHandler = new LinkedFileHandler(linkedFile, entry, databaseContext, preferences.getFilePreferences());
95101
this.databaseContext = databaseContext;
96102
this.entry = entry;
97103
this.dialogService = dialogService;
98104
this.taskExecutor = taskExecutor;
99105
this.externalFileTypes = externalFileTypes;
100-
this.xmpPreferences = xmpPreferences;
101106

102107
fileExistsValidator = new FunctionBasedValidator<>(
103108
linkedFile.linkProperty(),
104109
link -> {
105110
if (linkedFile.isOnlineLink()) {
106111
return true;
107112
} else {
108-
Optional<Path> path = FileHelper.find(databaseContext, link, filePreferences);
113+
Optional<Path> path = FileHelper.find(databaseContext, link, preferences.getFilePreferences());
109114
return path.isPresent() && Files.exists(path.get());
110115
}
111116
},
112117
ValidationMessage.warning(Localization.lang("Could not find file '%0'.", linkedFile.getLink())));
113118

114119
downloadOngoing.bind(downloadProgress.greaterThanOrEqualTo(0).and(downloadProgress.lessThan(1)));
115-
canWriteXMPMetadata.setValue(!linkedFile.isOnlineLink() && linkedFile.getFileType().equalsIgnoreCase("pdf"));
120+
isOfflinePdf.setValue(!linkedFile.isOnlineLink() && linkedFile.getFileType().equalsIgnoreCase("pdf"));
116121
}
117122

118-
public BooleanProperty canWriteXMPMetadataProperty() {
119-
return canWriteXMPMetadata;
123+
public BooleanProperty isOfflinePdfProperty() {
124+
return isOfflinePdf;
120125
}
121126

122127
public boolean isAutomaticallyFound() {
@@ -211,7 +216,7 @@ public void openFolder() {
211216
Optional<Path> resolvedPath = FileHelper.find(
212217
databaseContext,
213218
linkedFile.getLink(),
214-
filePreferences);
219+
preferences.getFilePreferences());
215220

216221
if (resolvedPath.isPresent()) {
217222
JabRefDesktop.openFolderAndSelectFile(resolvedPath.get());
@@ -246,7 +251,7 @@ public void renameFileToName(String targetFileName) {
246251
return;
247252
}
248253

249-
Optional<Path> file = linkedFile.findIn(databaseContext, filePreferences);
254+
Optional<Path> file = linkedFile.findIn(databaseContext, preferences.getFilePreferences());
250255
if (file.isPresent()) {
251256
performRenameWithConflictCheck(targetFileName);
252257
} else {
@@ -283,13 +288,13 @@ public void moveToDefaultDirectory() {
283288
}
284289

285290
// Get target folder
286-
Optional<Path> fileDir = databaseContext.getFirstExistingFileDir(filePreferences);
291+
Optional<Path> fileDir = databaseContext.getFirstExistingFileDir(preferences.getFilePreferences());
287292
if (fileDir.isEmpty()) {
288293
dialogService.showErrorDialogAndWait(Localization.lang("Move file"), Localization.lang("File directory is not set or does not exist!"));
289294
return;
290295
}
291296

292-
Optional<Path> file = linkedFile.findIn(databaseContext, filePreferences);
297+
Optional<Path> file = linkedFile.findIn(databaseContext, preferences.getFilePreferences());
293298
if ((file.isPresent())) {
294299
// Found the linked file, so move it
295300
try {
@@ -325,9 +330,9 @@ public boolean isGeneratedNameSameAsOriginal() {
325330
* @return true if suggested filepath is same as existing filepath.
326331
*/
327332
public boolean isGeneratedPathSameAsOriginal() {
328-
Optional<Path> newDir = databaseContext.getFirstExistingFileDir(filePreferences);
333+
Optional<Path> newDir = databaseContext.getFirstExistingFileDir(preferences.getFilePreferences());
329334

330-
Optional<Path> currentDir = linkedFile.findIn(databaseContext, filePreferences).map(Path::getParent);
335+
Optional<Path> currentDir = linkedFile.findIn(databaseContext, preferences.getFilePreferences()).map(Path::getParent);
331336

332337
BiPredicate<Path, Path> equality = (fileA, fileB) -> {
333338
try {
@@ -351,7 +356,7 @@ public void moveToDefaultDirectoryAndRename() {
351356
* successfully, does not exist in the first place or the user choose to remove it)
352357
*/
353358
public boolean delete() {
354-
Optional<Path> file = linkedFile.findIn(databaseContext, filePreferences);
359+
Optional<Path> file = linkedFile.findIn(databaseContext, preferences.getFilePreferences());
355360

356361
if (file.isEmpty()) {
357362
LOGGER.warn("Could not find file " + linkedFile.getLink());
@@ -395,13 +400,13 @@ public void edit() {
395400
public void writeXMPMetadata() {
396401
// Localization.lang("Writing XMP metadata...")
397402
BackgroundTask<Void> writeTask = BackgroundTask.wrap(() -> {
398-
Optional<Path> file = linkedFile.findIn(databaseContext, filePreferences);
403+
Optional<Path> file = linkedFile.findIn(databaseContext, preferences.getFilePreferences());
399404
if (file.isEmpty()) {
400405
// TODO: Print error message
401406
// Localization.lang("PDF does not exist");
402407
} else {
403408
try {
404-
XmpUtilWriter.writeXmp(file.get(), entry, databaseContext.getDatabase(), xmpPreferences);
409+
XmpUtilWriter.writeXmp(file.get(), entry, databaseContext.getDatabase(), preferences.getXmpPreferences());
405410
} catch (IOException | TransformerException ex) {
406411
// TODO: Print error message
407412
// Localization.lang("Error while writing") + " '" + file.toString() + "': " + ex;
@@ -421,7 +426,7 @@ public void download() {
421426
throw new UnsupportedOperationException("In order to download the file it has to be an online link");
422427
}
423428
try {
424-
Optional<Path> targetDirectory = databaseContext.getFirstExistingFileDir(filePreferences);
429+
Optional<Path> targetDirectory = databaseContext.getFirstExistingFileDir(preferences.getFilePreferences());
425430
if (targetDirectory.isEmpty()) {
426431
dialogService.showErrorDialogAndWait(Localization.lang("Download file"), Localization.lang("File directory is not set or does not exist!"));
427432
return;
@@ -443,7 +448,7 @@ public void download() {
443448
}
444449

445450
if (!isDuplicate) {
446-
LinkedFile newLinkedFile = LinkedFilesEditorViewModel.fromFile(destination, databaseContext.getFileDirectories(filePreferences), externalFileTypes);
451+
LinkedFile newLinkedFile = LinkedFilesEditorViewModel.fromFile(destination, databaseContext.getFileDirectories(preferences.getFilePreferences()), externalFileTypes);
447452
List<LinkedFile> linkedFiles = entry.getFiles();
448453

449454
entry.addLinkedFile(entry, linkedFile, newLinkedFile, linkedFiles);
@@ -495,7 +500,7 @@ public BackgroundTask<Path> prepareDownloadTask(Path targetDirectory, URLDownloa
495500
String suggestedTypeName = externalFileType.getName();
496501
linkedFile.setFileType(suggestedTypeName);
497502
String suggestedName = linkedFileHandler.getSuggestedFileName(externalFileType.getExtension());
498-
String fulltextDir = FileUtil.createDirNameFromPattern(databaseContext.getDatabase(), entry, filePreferences.getFileDirectoryPattern());
503+
String fulltextDir = FileUtil.createDirNameFromPattern(databaseContext.getDatabase(), entry, preferences.getFilePreferences().getFileDirectoryPattern());
499504
suggestedName = FileNameUniqueness.getNonOverWritingFileName(targetDirectory.resolve(fulltextDir), suggestedName);
500505
return targetDirectory.resolve(fulltextDir).resolve(suggestedName);
501506
})
@@ -538,4 +543,34 @@ public LinkedFile getFile() {
538543
public ValidationStatus fileExistsValidationStatus() {
539544
return fileExistsValidator.getValidationStatus();
540545
}
546+
547+
public void parsePdfMetadataAndShowMergeDialog() {
548+
linkedFile.findIn(databaseContext, preferences.getFilePreferences()).ifPresent(filePath -> {
549+
MultiMergeEntriesView dialog = new MultiMergeEntriesView(preferences, taskExecutor);
550+
dialog.addSource(Localization.lang("Entry"), entry);
551+
dialog.addSource(Localization.lang("Verbatim"), wrapImporterToSupplier(new PdfVerbatimBibTextImporter(preferences.getImportFormatPreferences()), filePath));
552+
dialog.addSource(Localization.lang("Embedded"), wrapImporterToSupplier(new PdfEmbeddedBibFileImporter(preferences.getImportFormatPreferences()), filePath));
553+
dialog.addSource("Grobid", wrapImporterToSupplier(new PdfGrobidImporter(preferences.getImportSettingsPreferences(), preferences.getImportFormatPreferences()), filePath));
554+
dialog.addSource(Localization.lang("XMP metadata"), wrapImporterToSupplier(new PdfXmpImporter(preferences.getXmpPreferences()), filePath));
555+
dialog.addSource(Localization.lang("Content"), wrapImporterToSupplier(new PdfContentImporter(preferences.getImportFormatPreferences()), filePath));
556+
dialog.showAndWait().ifPresent(newEntry -> {
557+
databaseContext.getDatabase().removeEntry(entry);
558+
databaseContext.getDatabase().insertEntry(newEntry);
559+
});
560+
});
561+
}
562+
563+
private Supplier<BibEntry> wrapImporterToSupplier(Importer importer, Path filePath) {
564+
return () -> {
565+
try {
566+
ParserResult parserResult = importer.importDatabase(filePath, preferences.getDefaultEncoding());
567+
if (parserResult.isInvalid() || parserResult.isEmpty() || !parserResult.getDatabase().hasEntries()) {
568+
return null;
569+
}
570+
return parserResult.getDatabase().getEntries().get(0);
571+
} catch (IOException e) {
572+
return null;
573+
}
574+
};
575+
}
541576
}

src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditor.java

+16-4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import javafx.scene.input.MouseEvent;
2424
import javafx.scene.input.TransferMode;
2525
import javafx.scene.layout.HBox;
26+
import javafx.scene.layout.Priority;
2627
import javafx.scene.text.Text;
2728

2829
import org.jabref.gui.DialogService;
@@ -34,6 +35,7 @@
3435
import org.jabref.gui.autocompleter.SuggestionProvider;
3536
import org.jabref.gui.copyfiles.CopySingleFileAction;
3637
import org.jabref.gui.icon.IconTheme;
38+
import org.jabref.gui.importer.GrobidOptInDialogHelper;
3739
import org.jabref.gui.keyboard.KeyBinding;
3840
import org.jabref.gui.util.BindingsHelper;
3941
import org.jabref.gui.util.TaskExecutor;
@@ -81,7 +83,7 @@ public LinkedFilesEditor(Field field,
8183

8284
ViewModelListCellFactory<LinkedFileViewModel> cellFactory = new ViewModelListCellFactory<LinkedFileViewModel>()
8385
.withStringTooltip(LinkedFileViewModel::getDescription)
84-
.withGraphic(LinkedFilesEditor::createFileDisplay)
86+
.withGraphic(this::createFileDisplay)
8587
.withContextMenu(this::createContextMenuForFile)
8688
.withOnMouseClickedEvent(this::handleItemMouseClick)
8789
.setOnDragDetected(this::handleOnDragDetected)
@@ -142,7 +144,7 @@ private void handleOnDragDropped(LinkedFileViewModel originalItem, DragEvent eve
142144
event.consume();
143145
}
144146

145-
private static Node createFileDisplay(LinkedFileViewModel linkedFile) {
147+
private Node createFileDisplay(LinkedFileViewModel linkedFile) {
146148
PseudoClass opacity = PseudoClass.getPseudoClass("opacity");
147149

148150
Node icon = linkedFile.getTypeIcon().getGraphicNode();
@@ -162,6 +164,7 @@ private static Node createFileDisplay(LinkedFileViewModel linkedFile) {
162164
progressIndicator.visibleProperty().bind(linkedFile.downloadOngoingProperty());
163165

164166
HBox info = new HBox(8);
167+
HBox.setHgrow(info, Priority.ALWAYS);
165168
info.setStyle("-fx-padding: 0.5em 0 0.5em 0;"); // To align with buttons below which also have 0.5em padding
166169
info.getChildren().setAll(icon, link, desc, progressIndicator);
167170

@@ -174,14 +177,23 @@ private static Node createFileDisplay(LinkedFileViewModel linkedFile) {
174177

175178
Button writeXMPMetadata = IconTheme.JabRefIcons.IMPORT.asButton();
176179
writeXMPMetadata.setTooltip(new Tooltip(Localization.lang("Write BibTeXEntry as XMP metadata to PDF.")));
177-
writeXMPMetadata.visibleProperty().bind(linkedFile.canWriteXMPMetadataProperty());
180+
writeXMPMetadata.visibleProperty().bind(linkedFile.isOfflinePdfProperty());
178181
writeXMPMetadata.setOnAction(event -> linkedFile.writeXMPMetadata());
179182
writeXMPMetadata.getStyleClass().setAll("icon-button");
180183

184+
Button parsePdfMetadata = IconTheme.JabRefIcons.FILE_SEARCH.asButton();
185+
parsePdfMetadata.setTooltip(new Tooltip(Localization.lang("Parse Metadata from PDF.")));
186+
parsePdfMetadata.visibleProperty().bind(linkedFile.isOfflinePdfProperty());
187+
parsePdfMetadata.setOnAction(event -> {
188+
GrobidOptInDialogHelper.showAndWaitIfUserIsUndecided(dialogService);
189+
linkedFile.parsePdfMetadataAndShowMergeDialog();
190+
});
191+
parsePdfMetadata.getStyleClass().setAll("icon-button");
192+
181193
HBox container = new HBox(10);
182194
container.setPrefHeight(Double.NEGATIVE_INFINITY);
183195

184-
container.getChildren().addAll(acceptAutoLinkedFile, info, writeXMPMetadata);
196+
container.getChildren().addAll(acceptAutoLinkedFile, info, writeXMPMetadata, parsePdfMetadata);
185197

186198
return container;
187199
}

src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditorViewModel.java

+5-10
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,7 @@ public LinkedFileViewModel fromFile(Path file) {
106106
databaseContext,
107107
taskExecutor,
108108
dialogService,
109-
preferences.getXmpPreferences(),
110-
preferences.getFilePreferences(),
109+
preferences,
111110
externalFileTypes);
112111
}
113112

@@ -123,8 +122,7 @@ private List<LinkedFileViewModel> parseToFileViewModel(String stringValue) {
123122
databaseContext,
124123
taskExecutor,
125124
dialogService,
126-
preferences.getXmpPreferences(),
127-
preferences.getFilePreferences(),
125+
preferences,
128126
externalFileTypes))
129127
.collect(Collectors.toList());
130128
}
@@ -154,8 +152,7 @@ public void addNewFile() {
154152
databaseContext,
155153
taskExecutor,
156154
dialogService,
157-
preferences.getXmpPreferences(),
158-
preferences.getFilePreferences(),
155+
preferences,
159156
externalFileTypes));
160157
});
161158
}
@@ -192,8 +189,7 @@ private List<LinkedFileViewModel> findAssociatedNotLinkedFiles(BibEntry entry) {
192189
databaseContext,
193190
taskExecutor,
194191
dialogService,
195-
preferences.getXmpPreferences(),
196-
preferences.getFilePreferences(),
192+
preferences,
197193
externalFileTypes);
198194
newLinkedFile.markAsAutomaticallyFound();
199195
result.add(newLinkedFile);
@@ -243,8 +239,7 @@ private void addFromURL(URL url) {
243239
databaseContext,
244240
taskExecutor,
245241
dialogService,
246-
preferences.getXmpPreferences(),
247-
preferences.getFilePreferences(),
242+
preferences,
248243
externalFileTypes);
249244
files.add(onlineFile);
250245
onlineFile.download();

0 commit comments

Comments
 (0)