From 2cdf085ca094e640073774764551296ff03d1f8f Mon Sep 17 00:00:00 2001 From: Tobias Diez Date: Sat, 17 Mar 2018 11:18:24 +0100 Subject: [PATCH] Provides download option in context menu and fixes #3614 (#3824) * Provides download option in context menu and fixes #3614 * Code cleanup * Fix checkstyle * Fix tests * Add java doc * Fix tests --- CHANGELOG.md | 2 + .../gui/externalfiles/FileDownloadTask.java | 4 + .../jabref/gui/fieldeditors/FieldEditors.java | 2 +- .../gui/fieldeditors/LinkedFileViewModel.java | 154 +++++++++++++++--- .../gui/fieldeditors/LinkedFilesEditor.java | 17 +- .../LinkedFilesEditorViewModel.java | 131 ++------------- .../org/jabref/gui/util/BackgroundTask.java | 21 ++- .../jabref/logic/importer/fetcher/ArXiv.java | 2 +- .../org/jabref/logic/util/io/FileUtil.java | 10 +- .../org/jabref/model/entry/LinkedFile.java | 12 ++ .../architecture/TestArchitectureTests.java | 2 + .../fieldeditors/LinkedFileViewModelTest.java | 14 +- 12 files changed, 212 insertions(+), 159 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11b9d9757dc..227667bea99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ For more details refer to the [field mapping help page](http://help.jabref.org/e - We added Facebook and Twitter icons in the toolbar to link to our [Facebook](https://www.facebook.com/JabRef/) and [Twitter](https://twitter.com/jabref_org) pages. - Renamed the _Review_ Tab into _Comments_ Tab - We no longer print empty lines when exporting an entry in RIS format [#3634](https://github.com/JabRef/jabref/issues/3634) +- We added the option to download linked URLs in the context menu in the entry editor. - We improved file saving so that hard links are now preserved when a save is performed [#2633](https://github.com/JabRef/jabref/issues/2633) - We changed the default dialog option when removing a [file link](http://help.jabref.org/en/FileLinks#adding-external-links-to-an-entry) from an entry. The new default removes the linked file from the entry instead of deleting the file from disk. [#3679](https://github.com/JabRef/jabref/issues/3679) @@ -45,6 +46,7 @@ The new default removes the linked file from the entry instead of deleting the f - We fixed an issue where pressing space caused the cursor to jump to the start of the text field. [#3471](https://github.com/JabRef/jabref/issues/3471) - We fixed the missing dot in the name of an exported file. [#3576](https://github.com/JabRef/jabref/issues/3576) - Autocompletion in the search bar can now be disabled via the preferences. [#3598](https://github.com/JabRef/jabref/issues/3598) +- We fixed an issue where the progress of an ongoing file download was not shown correctly. [#3614](https://github.com/JabRef/jabref/issues/3614) - We fixed an issue where odd linked files could not be selected in the entry editor. [#3639](https://github.com/JabRef/jabref/issues/3639) - We fixed and extended the RIS import functionality to cover more fields. [#3634](https://github.com/JabRef/jabref/issues/3634) [#2607](https://github.com/JabRef/jabref/issues/2607) - Chaining modifiers in BibTeX key pattern now works as described in the documentation. [#3648](https://github.com/JabRef/jabref/issues/3648) diff --git a/src/main/java/org/jabref/gui/externalfiles/FileDownloadTask.java b/src/main/java/org/jabref/gui/externalfiles/FileDownloadTask.java index 518d52c016f..890cd232f16 100644 --- a/src/main/java/org/jabref/gui/externalfiles/FileDownloadTask.java +++ b/src/main/java/org/jabref/gui/externalfiles/FileDownloadTask.java @@ -28,6 +28,10 @@ protected Void call() throws Exception { EasyBind.subscribe( inputStream.totalNumBytesReadProperty(), bytesRead -> updateProgress(bytesRead.longValue(), inputStream.getMaxNumBytes())); + + // Make sure directory exists since otherwise copy fails + Files.createDirectories(destination.getParent()); + Files.copy(inputStream, destination, StandardCopyOption.REPLACE_EXISTING); } diff --git a/src/main/java/org/jabref/gui/fieldeditors/FieldEditors.java b/src/main/java/org/jabref/gui/fieldeditors/FieldEditors.java index c292a009ffa..cc2b1fd32ac 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/FieldEditors.java +++ b/src/main/java/org/jabref/gui/fieldeditors/FieldEditors.java @@ -51,7 +51,7 @@ public static FieldEditorFX getForField(String fieldName, TaskExecutor taskExecu } else if (fieldExtras.contains(FieldProperty.OWNER)) { return new OwnerEditor(fieldName, preferences, suggestionProvider, fieldCheckers); } else if (fieldExtras.contains(FieldProperty.FILE_EDITOR)) { - return new LinkedFilesEditor(fieldName, dialogService, databaseContext, taskExecutor, suggestionProvider, fieldCheckers); + return new LinkedFilesEditor(fieldName, dialogService, databaseContext, taskExecutor, suggestionProvider, fieldCheckers, preferences); } else if (fieldExtras.contains(FieldProperty.YES_NO)) { return new OptionEditor<>(new YesNoEditorViewModel(fieldName, suggestionProvider, fieldCheckers)); } else if (fieldExtras.contains(FieldProperty.MONTH)) { diff --git a/src/main/java/org/jabref/gui/fieldeditors/LinkedFileViewModel.java b/src/main/java/org/jabref/gui/fieldeditors/LinkedFileViewModel.java index 6f44cec1c8b..5e52fec8ccb 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/LinkedFileViewModel.java +++ b/src/main/java/org/jabref/gui/fieldeditors/LinkedFileViewModel.java @@ -1,6 +1,7 @@ package org.jabref.gui.fieldeditors; import java.io.IOException; +import java.net.MalformedURLException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -17,6 +18,7 @@ import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.StringProperty; import javafx.scene.control.Alert.AlertType; import javafx.scene.control.ButtonType; @@ -25,20 +27,28 @@ import org.jabref.gui.DialogService; import org.jabref.gui.FXDialogService; import org.jabref.gui.desktop.JabRefDesktop; +import org.jabref.gui.externalfiles.DownloadExternalFile; +import org.jabref.gui.externalfiles.FileDownloadTask; import org.jabref.gui.externalfiletype.ExternalFileType; import org.jabref.gui.externalfiletype.ExternalFileTypes; import org.jabref.gui.filelist.FileListEntryEditor; import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.TaskExecutor; +import org.jabref.logic.cleanup.CleanupPreferences; import org.jabref.logic.cleanup.MoveFilesCleanup; import org.jabref.logic.cleanup.RenamePdfCleanup; +import org.jabref.logic.journals.JournalAbbreviationLoader; import org.jabref.logic.l10n.Localization; +import org.jabref.logic.layout.LayoutFormatterPreferences; +import org.jabref.logic.net.URLDownload; import org.jabref.logic.util.io.FileUtil; +import org.jabref.logic.xmp.XmpPreferences; import org.jabref.logic.xmp.XmpUtilWriter; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.LinkedFile; import org.jabref.model.metadata.FileDirectoryPreferences; +import org.jabref.preferences.JabRefPreferences; import de.jensd.fx.glyphs.GlyphIcons; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; @@ -60,20 +70,35 @@ public class LinkedFileViewModel extends AbstractViewModel { private final DialogService dialogService; private final BibEntry entry; private final TaskExecutor taskExecutor; + private final FileDirectoryPreferences fileDirectoryPreferences; + private final CleanupPreferences cleanupPreferences; + private final LayoutFormatterPreferences layoutFormatterPreferences; + private final XmpPreferences xmpPreferences; + private final String fileNamePattern; + /** + * @deprecated use {@link #LinkedFileViewModel(LinkedFile, BibEntry, BibDatabaseContext, TaskExecutor, DialogService, JabRefPreferences, JournalAbbreviationLoader)} instead + */ + @Deprecated public LinkedFileViewModel(LinkedFile linkedFile, BibEntry entry, BibDatabaseContext databaseContext, TaskExecutor taskExecutor) { - this(linkedFile, entry, databaseContext, taskExecutor, new FXDialogService()); + this(linkedFile, entry, databaseContext, taskExecutor, new FXDialogService(), Globals.prefs, Globals.journalAbbreviationLoader); } - protected LinkedFileViewModel(LinkedFile linkedFile, BibEntry entry, BibDatabaseContext databaseContext, - TaskExecutor taskExecutor, DialogService dialogService) { + public LinkedFileViewModel(LinkedFile linkedFile, BibEntry entry, BibDatabaseContext databaseContext, + TaskExecutor taskExecutor, DialogService dialogService, JabRefPreferences preferences, JournalAbbreviationLoader abbreviationLoader) { this.linkedFile = linkedFile; this.databaseContext = databaseContext; this.entry = entry; this.taskExecutor = taskExecutor; this.dialogService = dialogService; - downloadOngoing.bind(downloadProgress.greaterThanOrEqualTo(0).and(downloadProgress.lessThan(100))); + cleanupPreferences = preferences.getCleanupPreferences(abbreviationLoader); + layoutFormatterPreferences = preferences.getLayoutFormatterPreferences(abbreviationLoader); + xmpPreferences = preferences.getXMPPreferences(); + fileNamePattern = preferences.get(JabRefPreferences.IMPORT_FILENAMEPATTERN); + fileDirectoryPreferences = preferences.getFileDirectoryPreferences(); + + downloadOngoing.bind(downloadProgress.greaterThanOrEqualTo(0).and(downloadProgress.lessThan(1))); canWriteXMPMetadata.setValue(!linkedFile.isOnlineLink() && linkedFile.getFileType().equalsIgnoreCase("pdf")); } @@ -97,12 +122,12 @@ public DoubleProperty downloadProgressProperty() { return downloadProgress; } - public LinkedFile getFile() { - return linkedFile; + public StringProperty linkProperty() { + return linkedFile.linkProperty(); } - public String getLink() { - return linkedFile.getLink(); + public StringProperty descriptionProperty() { + return linkedFile.descriptionProperty(); } public String getDescription() { @@ -154,7 +179,7 @@ public void openFolder() { path = Paths.get(linkedFile.getLink()); } else { // relative to file folder - for (Path folder : databaseContext.getFileDirectoriesAsPaths(Globals.prefs.getFileDirectoryPreferences())) { + for (Path folder : databaseContext.getFileDirectoriesAsPaths(fileDirectoryPreferences)) { Path file = folder.resolve(linkedFile.getLink()); if (Files.exists(file)) { path = file; @@ -177,7 +202,7 @@ public void rename() { // Cannot rename remote links return; } - Optional fileDir = databaseContext.getFirstExistingFileDir(Globals.prefs.getFileDirectoryPreferences()); + Optional fileDir = databaseContext.getFirstExistingFileDir(fileDirectoryPreferences); if (!fileDir.isPresent()) { dialogService.showErrorDialogAndWait( Localization.lang("Rename file"), @@ -185,13 +210,13 @@ public void rename() { return; } - Optional file = linkedFile.findIn(databaseContext, Globals.prefs.getFileDirectoryPreferences()); + Optional file = linkedFile.findIn(databaseContext, fileDirectoryPreferences); if ((file.isPresent()) && Files.exists(file.get())) { RenamePdfCleanup pdfCleanup = new RenamePdfCleanup(false, databaseContext, - Globals.prefs.getCleanupPreferences(Globals.journalAbbreviationLoader).getFileNamePattern(), - Globals.prefs.getLayoutFormatterPreferences(Globals.journalAbbreviationLoader), - Globals.prefs.getFileDirectoryPreferences(), linkedFile); + cleanupPreferences.getFileNamePattern(), + layoutFormatterPreferences, + fileDirectoryPreferences, linkedFile); String targetFileName = pdfCleanup.getTargetFileName(linkedFile, entry); @@ -249,7 +274,7 @@ public void moveToDefaultDirectory() { } // Get target folder - Optional fileDir = databaseContext.getFirstExistingFileDir(Globals.prefs.getFileDirectoryPreferences()); + Optional fileDir = databaseContext.getFirstExistingFileDir(fileDirectoryPreferences); if (!fileDir.isPresent()) { dialogService.showErrorDialogAndWait( Localization.lang("Move file"), @@ -257,13 +282,13 @@ public void moveToDefaultDirectory() { return; } - Optional file = linkedFile.findIn(databaseContext, Globals.prefs.getFileDirectoryPreferences()); + Optional file = linkedFile.findIn(databaseContext, fileDirectoryPreferences); if ((file.isPresent()) && Files.exists(file.get())) { // Linked file exists, so move it MoveFilesCleanup moveFiles = new MoveFilesCleanup(databaseContext, - Globals.prefs.getCleanupPreferences(Globals.journalAbbreviationLoader).getFileDirPattern(), - Globals.prefs.getFileDirectoryPreferences(), - Globals.prefs.getLayoutFormatterPreferences(Globals.journalAbbreviationLoader), linkedFile); + cleanupPreferences.getFileDirPattern(), + fileDirectoryPreferences, + layoutFormatterPreferences, linkedFile); boolean confirm = dialogService.showConfirmationDialogAndWait( Localization.lang("Move file"), @@ -325,13 +350,13 @@ public void edit() { public void writeXMPMetadata() { // Localization.lang("Writing XMP-metadata...") BackgroundTask writeTask = BackgroundTask.wrap(() -> { - Optional file = linkedFile.findIn(databaseContext, Globals.prefs.getFileDirectoryPreferences()); + Optional file = linkedFile.findIn(databaseContext, fileDirectoryPreferences); if (!file.isPresent()) { // TODO: Print error message // Localization.lang("PDF does not exist"); } else { try { - XmpUtilWriter.writeXmp(file.get(), entry, databaseContext.getDatabase(), Globals.prefs.getXMPPreferences()); + XmpUtilWriter.writeXmp(file.get(), entry, databaseContext.getDatabase(), xmpPreferences); } catch (IOException | TransformerException ex) { // TODO: Print error message // Localization.lang("Error while writing") + " '" + file.toString() + "': " + ex; @@ -345,4 +370,91 @@ public void writeXMPMetadata() { // TODO: Show progress taskExecutor.execute(writeTask); } + + public void download() { + if (!linkedFile.isOnlineLink()) { + throw new UnsupportedOperationException("In order to download the file it has to be an online link"); + } + + try { + URLDownload urlDownload = new URLDownload(linkedFile.getLink()); + Optional suggestedType = inferFileType(urlDownload); + String suggestedTypeName = suggestedType.map(ExternalFileType::getName).orElse(""); + linkedFile.setFileType(suggestedTypeName); + + Optional targetDirectory = databaseContext.getFirstExistingFileDir(fileDirectoryPreferences); + if (!targetDirectory.isPresent()) { + dialogService.showErrorDialogAndWait( + Localization.lang("Download file"), + Localization.lang("File directory is not set or does not exist!")); + return; + } + String suffix = suggestedType.map(ExternalFileType::getExtension).orElse(""); + String suggestedName = getSuggestedFileName(suffix); + Path destination = targetDirectory.get().resolve(suggestedName); + + BackgroundTask downloadTask = new FileDownloadTask(urlDownload.getSource(), destination) + .onSuccess(event -> { + LinkedFile newLinkedFile = LinkedFilesEditorViewModel.fromFile(destination, databaseContext.getFileDirectoriesAsPaths(fileDirectoryPreferences)); + linkedFile.setLink(newLinkedFile.getLink()); + linkedFile.setFileType(newLinkedFile.getFileType()); + }) + .onFailure(ex -> dialogService.showErrorDialogAndWait("Download failed", ex)); + + downloadProgress.bind(downloadTask.workDonePercentageProperty()); + taskExecutor.execute(downloadTask); + } catch (MalformedURLException exception) { + dialogService.showErrorDialogAndWait( + Localization.lang("Invalid URL"), + exception); + } + } + + private Optional inferFileType(URLDownload urlDownload) { + Optional suggestedType = inferFileTypeFromMimeType(urlDownload); + + // If we did not find a file type from the MIME type, try based on extension: + if (!suggestedType.isPresent()) { + suggestedType = inferFileTypeFromURL(urlDownload.getSource().toExternalForm()); + } + return suggestedType; + } + + private Optional inferFileTypeFromMimeType(URLDownload urlDownload) { + try { + // TODO: what if this takes long time? + String mimeType = urlDownload.getMimeType(); // Read MIME type + if (mimeType != null) { + LOGGER.debug("MIME Type suggested: " + mimeType); + return ExternalFileTypes.getInstance().getExternalFileTypeByMimeType(mimeType); + } else { + return Optional.empty(); + } + } catch (IOException ex) { + LOGGER.debug("Error while inferring MIME type for URL " + urlDownload.getSource(), ex); + return Optional.empty(); + } + } + + private Optional inferFileTypeFromURL(String url) { + String extension = DownloadExternalFile.getSuffix(url); + if (extension != null) { + return ExternalFileTypes.getInstance().getExternalFileTypeByExt(extension); + } else { + return Optional.empty(); + } + } + + private String getSuggestedFileName(String suffix) { + String plannedName = FileUtil.createFileNameFromPattern(databaseContext.getDatabase(), entry, fileNamePattern); + + if (!suffix.isEmpty()) { + plannedName += "." + suffix; + } + return plannedName; + } + + public LinkedFile getFile() { + return linkedFile; + } } diff --git a/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditor.java b/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditor.java index 306bb3b148c..5396bba3d4e 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditor.java +++ b/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditor.java @@ -41,6 +41,7 @@ import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.LinkedFile; +import org.jabref.preferences.JabRefPreferences; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import de.jensd.fx.glyphs.materialdesignicons.utils.MaterialDesignIconFactory; @@ -50,8 +51,8 @@ public class LinkedFilesEditor extends HBox implements FieldEditorFX { @FXML private final LinkedFilesEditorViewModel viewModel; @FXML private ListView listView; - public LinkedFilesEditor(String fieldName, DialogService dialogService, BibDatabaseContext databaseContext, TaskExecutor taskExecutor, AutoCompleteSuggestionProvider suggestionProvider, FieldCheckers fieldCheckers) { - this.viewModel = new LinkedFilesEditorViewModel(fieldName, suggestionProvider, dialogService, databaseContext, taskExecutor, fieldCheckers); + public LinkedFilesEditor(String fieldName, DialogService dialogService, BibDatabaseContext databaseContext, TaskExecutor taskExecutor, AutoCompleteSuggestionProvider suggestionProvider, FieldCheckers fieldCheckers, JabRefPreferences preferences) { + this.viewModel = new LinkedFilesEditorViewModel(fieldName, suggestionProvider, dialogService, databaseContext, taskExecutor, fieldCheckers, preferences); ControlHelper.loadFXMLForControl(this); @@ -150,8 +151,10 @@ private void handleOnDragDropped(LinkedFileViewModel originalItem, DragEvent eve private static Node createFileDisplay(LinkedFileViewModel linkedFile) { Text icon = MaterialDesignIconFactory.get().createIcon(linkedFile.getTypeIcon()); icon.setOnMouseClicked(event -> linkedFile.open()); - Text link = new Text(linkedFile.getLink()); - Text desc = new Text(linkedFile.getDescription()); + Text link = new Text(); + link.textProperty().bind(linkedFile.linkProperty()); + Text desc = new Text(); + desc.textProperty().bind(linkedFile.descriptionProperty()); ProgressBar progressIndicator = new ProgressBar(); progressIndicator.progressProperty().bind(linkedFile.downloadProgressProperty()); @@ -242,6 +245,9 @@ private ContextMenu createContextMenuForFile(LinkedFileViewModel linkedFile) { MenuItem openFolder = new MenuItem(Localization.lang("Open folder")); openFolder.setOnAction(event -> linkedFile.openFolder()); + MenuItem download = new MenuItem(Localization.lang("Download file")); + download.setOnAction(event -> linkedFile.download()); + MenuItem renameFile = new MenuItem(Localization.lang("Rename file")); renameFile.setOnAction(event -> linkedFile.rename()); renameFile.setDisable(linkedFile.getFile().isOnlineLink()); @@ -261,6 +267,9 @@ private ContextMenu createContextMenuForFile(LinkedFileViewModel linkedFile) { menu.getItems().add(new SeparatorMenuItem()); menu.getItems().addAll(openFile, openFolder); menu.getItems().add(new SeparatorMenuItem()); + if (linkedFile.getFile().isOnlineLink()) { + menu.getItems().add(download); + } menu.getItems().addAll(renameFile, moveFile, deleteLink, deleteFile); return menu; diff --git a/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditorViewModel.java b/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditorViewModel.java index 6db271022c6..75844815286 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditorViewModel.java +++ b/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditorViewModel.java @@ -17,12 +17,9 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import org.jabref.Globals; import org.jabref.gui.DialogService; import org.jabref.gui.autocompleter.AutoCompleteSuggestionProvider; import org.jabref.gui.externalfiles.AutoSetFileLinksUtil; -import org.jabref.gui.externalfiles.DownloadExternalFile; -import org.jabref.gui.externalfiles.FileDownloadTask; import org.jabref.gui.externalfiletype.ExternalFileType; import org.jabref.gui.externalfiletype.ExternalFileTypes; import org.jabref.gui.externalfiletype.UnknownExternalFileType; @@ -33,8 +30,6 @@ import org.jabref.logic.importer.FulltextFetchers; import org.jabref.logic.integrity.FieldCheckers; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.net.URLDownload; -import org.jabref.logic.util.OS; import org.jabref.logic.util.io.FileUtil; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; @@ -44,25 +39,23 @@ import org.jabref.model.util.FileHelper; import org.jabref.preferences.JabRefPreferences; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - public class LinkedFilesEditorViewModel extends AbstractEditorViewModel { - private static final Logger LOGGER = LoggerFactory.getLogger(LinkedFilesEditorViewModel.class); - private final ListProperty files = new SimpleListProperty<>(FXCollections.observableArrayList(LinkedFileViewModel::getObservables)); private final BooleanProperty fulltextLookupInProgress = new SimpleBooleanProperty(false); private final DialogService dialogService; private final BibDatabaseContext databaseContext; private final TaskExecutor taskExecutor; + private final JabRefPreferences preferences; - public LinkedFilesEditorViewModel(String fieldName, AutoCompleteSuggestionProvider suggestionProvider, DialogService dialogService, BibDatabaseContext databaseContext, TaskExecutor taskExecutor, FieldCheckers fieldCheckers) { + public LinkedFilesEditorViewModel(String fieldName, AutoCompleteSuggestionProvider suggestionProvider, DialogService dialogService, BibDatabaseContext databaseContext, TaskExecutor taskExecutor, FieldCheckers fieldCheckers, JabRefPreferences preferences) { super(fieldName, suggestionProvider, fieldCheckers); this.dialogService = dialogService; this.databaseContext = databaseContext; this.taskExecutor = taskExecutor; + this.preferences = preferences; + BindingsHelper.bindContentBidirectional( files, text, @@ -86,7 +79,7 @@ private static String getStringRepresentation(List files) { * * TODO: Move this method to {@link LinkedFile} as soon as {@link ExternalFileType} lives in model. */ - private static LinkedFile fromFile(Path file, List fileDirectories) { + public static LinkedFile fromFile(Path file, List fileDirectories) { String fileExtension = FileHelper.getFileExtension(file).orElse(""); ExternalFileType suggestedFileType = ExternalFileTypes.getInstance() .getExternalFileTypeByExt(fileExtension) @@ -96,7 +89,7 @@ private static LinkedFile fromFile(Path file, List fileDirectories) { } public LinkedFileViewModel fromFile(Path file) { - List fileDirectories = databaseContext.getFileDirectoriesAsPaths(Globals.prefs.getFileDirectoryPreferences()); + List fileDirectories = databaseContext.getFileDirectoriesAsPaths(preferences.getFileDirectoryPreferences()); LinkedFile linkedFile = fromFile(file, fileDirectories); return new LinkedFileViewModel(linkedFile, entry, databaseContext, taskExecutor); @@ -127,14 +120,14 @@ public ListProperty filesProperty() { } public void addNewFile() { - Path workingDirectory = databaseContext.getFirstExistingFileDir(Globals.prefs.getFileDirectoryPreferences()) - .orElse(Paths.get(Globals.prefs.get(JabRefPreferences.WORKING_DIRECTORY))); + Path workingDirectory = databaseContext.getFirstExistingFileDir(preferences.getFileDirectoryPreferences()) + .orElse(Paths.get(preferences.get(JabRefPreferences.WORKING_DIRECTORY))); FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder() .withInitialDirectory(workingDirectory) .build(); - List fileDirectories = databaseContext.getFileDirectoriesAsPaths(Globals.prefs.getFileDirectoryPreferences()); + List fileDirectories = databaseContext.getFileDirectoriesAsPaths(preferences.getFileDirectoryPreferences()); dialogService.showFileOpenDialog(fileDialogConfiguration).ifPresent( newFile -> { LinkedFile newLinkedFile = fromFile(newFile, fileDirectories); @@ -149,7 +142,7 @@ public void bindToEntry(BibEntry entry) { if (entry != null) { BackgroundTask> findAssociatedNotLinkedFiles = BackgroundTask .wrap(() -> findAssociatedNotLinkedFiles(entry)) - .onSuccess(newFiles -> files.addAll(newFiles)); + .onSuccess(files::addAll); taskExecutor.execute(findAssociatedNotLinkedFiles); } } @@ -160,7 +153,7 @@ public void bindToEntry(BibEntry entry) { private List findAssociatedNotLinkedFiles(BibEntry entry) { List result = new ArrayList<>(); - AutoSetFileLinksUtil util = new AutoSetFileLinksUtil(databaseContext, Globals.prefs.getFileDirectoryPreferences(), Globals.prefs.getAutoLinkPreferences(), ExternalFileTypes.getInstance()); + AutoSetFileLinksUtil util = new AutoSetFileLinksUtil(databaseContext, preferences.getFileDirectoryPreferences(), preferences.getAutoLinkPreferences(), ExternalFileTypes.getInstance()); try { List linkedFiles = util.findAssociatedNotLinkedFiles(entry); for (LinkedFile linkedFile : linkedFiles) { @@ -176,7 +169,7 @@ private List findAssociatedNotLinkedFiles(BibEntry entry) { } public void fetchFulltext() { - FulltextFetchers fetcher = new FulltextFetchers(Globals.prefs.getImportFormatPreferences()); + FulltextFetchers fetcher = new FulltextFetchers(preferences.getImportFormatPreferences()); BackgroundTask .wrap(() -> fetcher.findFullTextPDF(entry)) .onRunning(() -> fulltextLookupInProgress.setValue(true)) @@ -207,107 +200,17 @@ public void addFromURL() { } private void addFromURL(URL url) { - URLDownload urlDownload = new URLDownload(url); - - Optional suggestedType = inferFileType(urlDownload); - String suggestedTypeName = suggestedType.map(ExternalFileType::getName).orElse(""); - List fileDirectories = databaseContext.getFileDirectoriesAsPaths(Globals.prefs.getFileDirectoryPreferences()); - Path destination = constructSuggestedPath(suggestedType, fileDirectories); - - LinkedFileViewModel temporaryDownloadFile = new LinkedFileViewModel( - new LinkedFile("", url, suggestedTypeName), entry, databaseContext, taskExecutor); - files.add(temporaryDownloadFile); - BackgroundTask downloadTask = new FileDownloadTask(url, destination) - .onSuccess(event -> { - files.remove(temporaryDownloadFile); - LinkedFile newLinkedFile = fromFile(destination, fileDirectories); - files.add(new LinkedFileViewModel(newLinkedFile, entry, databaseContext, taskExecutor)); - }) - .onFailure(ex -> dialogService.showErrorDialogAndWait("", ex)); - - temporaryDownloadFile.downloadProgressProperty().bind(downloadTask.workDoneProperty()); - taskExecutor.execute(downloadTask); - } - - private Optional inferFileType(URLDownload urlDownload) { - Optional suggestedType = inferFileTypeFromMimeType(urlDownload); - - // If we did not find a file type from the MIME type, try based on extension: - if (!suggestedType.isPresent()) { - suggestedType = inferFileTypeFromURL(urlDownload.getSource().toExternalForm()); - } - return suggestedType; - } - - private Path constructSuggestedPath(Optional suggestedType, List fileDirectories) { - String suffix = suggestedType.map(ExternalFileType::getExtension).orElse(""); - String suggestedName = getSuggestedFileName(suffix); - Path directory; - if (fileDirectories.isEmpty()) { - directory = null; - } else { - directory = fileDirectories.get(0); - } - final Path suggestDir = directory == null ? Paths.get(System.getProperty("user.home")) : directory; - return suggestDir.resolve(suggestedName); - } - - private Optional inferFileTypeFromMimeType(URLDownload urlDownload) { - try { - // TODO: what if this takes long time? - String mimeType = urlDownload.getMimeType(); // Read MIME type - if (mimeType != null) { - LOGGER.debug("MIME Type suggested: " + mimeType); - return ExternalFileTypes.getInstance().getExternalFileTypeByMimeType(mimeType); - } else { - return Optional.empty(); - } - } catch (IOException ex) { - LOGGER.debug("Error while inferring MIME type for URL " + urlDownload.getSource(), ex); - return Optional.empty(); - } - } - - private Optional inferFileTypeFromURL(String url) { - String extension = DownloadExternalFile.getSuffix(url); - if (extension != null) { - return ExternalFileTypes.getInstance().getExternalFileTypeByExt(extension); - } else { - return Optional.empty(); - } - } - - private String getSuggestedFileName(String suffix) { - String plannedName = FileUtil.createFileNameFromPattern(databaseContext.getDatabase(), entry, - Globals.prefs.get(JabRefPreferences.IMPORT_FILENAMEPATTERN)); - - if (!suffix.isEmpty()) { - plannedName += "." + suffix; - } - - /* - * [ 1548875 ] download pdf produces unsupported filename - * - * http://sourceforge.net/tracker/index.php?func=detail&aid=1548875&group_id=92314&atid=600306 - * FIXME: rework this! just allow alphanumeric stuff or so? - * https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#naming_conventions - * http://superuser.com/questions/358855/what-characters-are-safe-in-cross-platform-file-names-for-linux-windows-and-os - * https://support.apple.com/en-us/HT202808 - */ - if (OS.WINDOWS) { - plannedName = plannedName.replaceAll("\\?|\\*|\\<|\\>|\\||\\\"|\\:|\\.$|\\[|\\]", ""); - } else if (OS.OS_X) { - plannedName = plannedName.replace(":", ""); - } - - return plannedName; + LinkedFileViewModel onlineFile = new LinkedFileViewModel( + new LinkedFile("", url, ""), entry, databaseContext, taskExecutor); + files.add(onlineFile); + onlineFile.download(); } public void deleteFile(LinkedFileViewModel file) { if (file.getFile().isOnlineLink()) { removeFileLink(file); } else { - boolean deleteSuccessful = file.delete(Globals.prefs.getFileDirectoryPreferences()); + boolean deleteSuccessful = file.delete(preferences.getFileDirectoryPreferences()); if (deleteSuccessful) { files.remove(file); } diff --git a/src/main/java/org/jabref/gui/util/BackgroundTask.java b/src/main/java/org/jabref/gui/util/BackgroundTask.java index 5a08cabed04..a0ba66966b4 100644 --- a/src/main/java/org/jabref/gui/util/BackgroundTask.java +++ b/src/main/java/org/jabref/gui/util/BackgroundTask.java @@ -26,10 +26,10 @@ public abstract class BackgroundTask { private Consumer onException; private Runnable onFinished; private ObjectProperty progress = new SimpleObjectProperty<>(new BackgroundProgress(0, 0)); - private DoubleProperty workDone = new SimpleDoubleProperty(0); + private DoubleProperty workDonePercentage = new SimpleDoubleProperty(0); public BackgroundTask() { - workDone.bind(EasyBind.map(progressProperty(), BackgroundTask.BackgroundProgress::getWorkDone)); + workDonePercentage.bind(EasyBind.map(progress, BackgroundTask.BackgroundProgress::getWorkDonePercentage)); } public static BackgroundTask wrap(Callable callable) { @@ -41,12 +41,12 @@ protected V call() throws Exception { }; } - public double getWorkDone() { - return workDone.get(); + public double getWorkDonePercentage() { + return workDonePercentage.get(); } - public DoubleProperty workDoneProperty() { - return workDone; + public DoubleProperty workDonePercentageProperty() { + return workDonePercentage; } public BackgroundProgress getProgress() { @@ -130,7 +130,6 @@ public class BackgroundProgress { private final double max; public BackgroundProgress(double workDone, double max) { - this.workDone = workDone; this.max = max; } @@ -142,5 +141,13 @@ public double getWorkDone() { public double getMax() { return max; } + + public double getWorkDonePercentage() { + if (max == 0) { + return 0; + } else { + return workDone / max; + } + } } } diff --git a/src/main/java/org/jabref/logic/importer/fetcher/ArXiv.java b/src/main/java/org/jabref/logic/importer/fetcher/ArXiv.java index 666b773c2e3..f91d3d7ec7a 100644 --- a/src/main/java/org/jabref/logic/importer/fetcher/ArXiv.java +++ b/src/main/java/org/jabref/logic/importer/fetcher/ArXiv.java @@ -398,7 +398,7 @@ public BibEntry toBibEntry(Character keywordDelimiter) { primaryCategory.ifPresent(category -> bibEntry.setField(FieldName.EPRINTCLASS, category)); journalReferenceText.ifPresent(journal -> bibEntry.setField(FieldName.JOURNALTITLE, journal)); getPdfUrl().ifPresent(url -> bibEntry - .setFiles(Collections.singletonList(new LinkedFile("online", url, "PDF")))); + .setFiles(Collections.singletonList(new LinkedFile("", url, "PDF")))); return bibEntry; } } diff --git a/src/main/java/org/jabref/logic/util/io/FileUtil.java b/src/main/java/org/jabref/logic/util/io/FileUtil.java index 5c95b0565d2..8a9828a0b7b 100644 --- a/src/main/java/org/jabref/logic/util/io/FileUtil.java +++ b/src/main/java/org/jabref/logic/util/io/FileUtil.java @@ -258,7 +258,7 @@ public static List getListOfLinkedFiles(List bes, List fil * @param fileNamePattern the filename pattern * @param prefs the layout preferences * @return a suggested fileName - * @Deprecated use String createFileNameFromPattern(BibDatabase database, BibEntry entry, String fileNamePattern ) instead. + * @deprecated use String createFileNameFromPattern(BibDatabase database, BibEntry entry, String fileNamePattern ) instead. */ @Deprecated public static String createFileNameFromPattern(BibDatabase database, BibEntry entry, String fileNamePattern, @@ -295,7 +295,7 @@ public static String createFileNameFromPattern(BibDatabase database, BibEntry en public static String createFileNameFromPattern(BibDatabase database, BibEntry entry, String fileNamePattern) { String targetName = BracketedPattern.expandBrackets(fileNamePattern, ';', entry, database); - if ((targetName == null) || targetName.isEmpty()) { + if (targetName.isEmpty()) { targetName = entry.getCiteKeyOptional().orElse("default"); } @@ -313,11 +313,9 @@ public static String createFileNameFromPattern(BibDatabase database, BibEntry en * @return a suggested fileName */ public static String createDirNameFromPattern(BibDatabase database, BibEntry entry, String fileNamePattern) { - String targetName = null; - - targetName = BracketedPattern.expandBrackets(fileNamePattern, ';', entry, database); + String targetName = BracketedPattern.expandBrackets(fileNamePattern, ';', entry, database); - if ((targetName == null) || targetName.isEmpty()) { + if (targetName.isEmpty()) { targetName = entry.getCiteKeyOptional().orElse("default"); } diff --git a/src/main/java/org/jabref/model/entry/LinkedFile.java b/src/main/java/org/jabref/model/entry/LinkedFile.java index b5532b024a0..0d361eaca2d 100644 --- a/src/main/java/org/jabref/model/entry/LinkedFile.java +++ b/src/main/java/org/jabref/model/entry/LinkedFile.java @@ -41,6 +41,18 @@ public LinkedFile(String description, URL link, String fileType) { this(description, Objects.requireNonNull(link).toString(), fileType); } + public StringProperty descriptionProperty() { + return description; + } + + public StringProperty linkProperty() { + return link; + } + + public StringProperty fileTypeProperty() { + return fileType; + } + public String getFileType() { return fileType.get(); } diff --git a/src/test/java/org/jabref/architecture/TestArchitectureTests.java b/src/test/java/org/jabref/architecture/TestArchitectureTests.java index 04d5b0e8cf0..ae520b6fee9 100644 --- a/src/test/java/org/jabref/architecture/TestArchitectureTests.java +++ b/src/test/java/org/jabref/architecture/TestArchitectureTests.java @@ -28,6 +28,7 @@ public class TestArchitectureTests { private static final String CLASS_ORG_JABREF_PREFERENCES_MIGRATIONS_TEST = "PreferencesMigrationsTest"; private static final String CLASS_ORG_JABREF_UPDATE_TIMESTAMP_LISTENER_TEST = "UpdateTimestampListenerTest"; private static final String CLASS_ORG_JABREF_ENTRY_EDITOR_TEST = "EntryEditorTest"; + private static final String CLASS_ORG_JABREF_LINKED_FILE_VIEW_MODEL_TEST = "LinkedFileViewModelTest"; private final String forbiddenPackage; @@ -43,6 +44,7 @@ public TestArchitectureTests(String forbiddenPackage) { exceptions.add(CLASS_ORG_JABREF_PREFERENCES_MIGRATIONS_TEST); exceptions.add(CLASS_ORG_JABREF_UPDATE_TIMESTAMP_LISTENER_TEST); exceptions.add(CLASS_ORG_JABREF_ENTRY_EDITOR_TEST); + exceptions.add(CLASS_ORG_JABREF_LINKED_FILE_VIEW_MODEL_TEST); } @Parameterized.Parameters(name = "tests independent of {0}?") diff --git a/src/test/java/org/jabref/gui/fieldeditors/LinkedFileViewModelTest.java b/src/test/java/org/jabref/gui/fieldeditors/LinkedFileViewModelTest.java index fd6d832c671..672fc8612e7 100644 --- a/src/test/java/org/jabref/gui/fieldeditors/LinkedFileViewModelTest.java +++ b/src/test/java/org/jabref/gui/fieldeditors/LinkedFileViewModelTest.java @@ -9,10 +9,12 @@ import org.jabref.gui.DialogService; import org.jabref.gui.util.TaskExecutor; +import org.jabref.logic.journals.JournalAbbreviationLoader; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.LinkedFile; import org.jabref.model.metadata.FileDirectoryPreferences; +import org.jabref.preferences.JabRefPreferences; import org.junit.Before; import org.junit.Rule; @@ -33,6 +35,8 @@ public class LinkedFileViewModelTest { @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); + private final JabRefPreferences preferences = mock(JabRefPreferences.class); + private final JournalAbbreviationLoader abbreviationLoader = mock(JournalAbbreviationLoader.class); private LinkedFile linkedFile; private BibEntry entry; private BibDatabaseContext databaseContext; @@ -55,7 +59,7 @@ public void deleteWhenFilePathNotPresentReturnsTrue() { linkedFile = spy(new LinkedFile("", "nonexistent file", "")); doReturn(Optional.empty()).when(linkedFile).findIn(any(BibDatabaseContext.class), any(FileDirectoryPreferences.class)); - LinkedFileViewModel viewModel = new LinkedFileViewModel(linkedFile, entry, databaseContext, taskExecutor, dialogService); + LinkedFileViewModel viewModel = new LinkedFileViewModel(linkedFile, entry, databaseContext, taskExecutor, dialogService, preferences, abbreviationLoader); boolean removed = viewModel.delete(fileDirectoryPreferences); assertTrue(removed); @@ -74,7 +78,7 @@ public void deleteWhenRemoveChosenReturnsTrue() throws IOException { any(ButtonType.class), any(ButtonType.class))).thenAnswer(invocation -> Optional.of(invocation.getArgument(3))); // first vararg - remove button - LinkedFileViewModel viewModel = new LinkedFileViewModel(linkedFile, entry, databaseContext, taskExecutor, dialogService); + LinkedFileViewModel viewModel = new LinkedFileViewModel(linkedFile, entry, databaseContext, taskExecutor, dialogService, preferences, abbreviationLoader); boolean removed = viewModel.delete(fileDirectoryPreferences); assertTrue(removed); @@ -93,7 +97,7 @@ public void deleteWhenDeleteChosenReturnsTrueAndDeletesFile() throws IOException any(ButtonType.class), any(ButtonType.class))).thenAnswer(invocation -> Optional.of(invocation.getArgument(4))); // second vararg - delete button - LinkedFileViewModel viewModel = new LinkedFileViewModel(linkedFile, entry, databaseContext, taskExecutor, dialogService); + LinkedFileViewModel viewModel = new LinkedFileViewModel(linkedFile, entry, databaseContext, taskExecutor, dialogService, preferences, abbreviationLoader); boolean removed = viewModel.delete(fileDirectoryPreferences); assertTrue(removed); @@ -111,7 +115,7 @@ public void deleteWhenDeleteChosenAndFileMissingReturnsFalse() throws IOExceptio any(ButtonType.class), any(ButtonType.class))).thenAnswer(invocation -> Optional.of(invocation.getArgument(4))); // second vararg - delete button - LinkedFileViewModel viewModel = new LinkedFileViewModel(linkedFile, entry, databaseContext, taskExecutor, dialogService); + LinkedFileViewModel viewModel = new LinkedFileViewModel(linkedFile, entry, databaseContext, taskExecutor, dialogService, preferences, abbreviationLoader); boolean removed = viewModel.delete(fileDirectoryPreferences); verify(dialogService).showErrorDialogAndWait(anyString(), anyString()); @@ -130,7 +134,7 @@ public void deleteWhenDialogCancelledReturnsFalse() throws IOException { any(ButtonType.class), any(ButtonType.class))).thenAnswer(invocation -> Optional.of(invocation.getArgument(5))); // third vararg - cancel button - LinkedFileViewModel viewModel = new LinkedFileViewModel(linkedFile, entry, databaseContext, taskExecutor, dialogService); + LinkedFileViewModel viewModel = new LinkedFileViewModel(linkedFile, entry, databaseContext, taskExecutor, dialogService, preferences, abbreviationLoader); boolean removed = viewModel.delete(fileDirectoryPreferences); assertFalse(removed);