package org.jabref.gui.externalfiles; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Paths; import java.util.List; import java.util.Optional; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import org.jabref.Globals; import org.jabref.JabRefExecutorService; import org.jabref.gui.JabRefFrame; import org.jabref.gui.externalfiletype.ExternalFileType; import org.jabref.gui.externalfiletype.ExternalFileTypes; import org.jabref.gui.filelist.FileListEntry; import org.jabref.gui.filelist.FileListEntryEditor; 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; import org.jabref.preferences.JabRefPreferences; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * This class handles the download of an external file. Typically called when the user clicks * the "Download" button in a FileListEditor shown in an EntryEditor. * <p/> * The FileListEditor constructs the DownloadExternalFile instance, then calls the download() * method passing a reference to itself as a callback. The download() method asks for the URL, * then starts the download. When the download is completed, it calls the downloadCompleted() * method on the callback FileListEditor, which then needs to take care of linking to the file. * The local filename is passed as an argument to the downloadCompleted() method. * <p/> * If the download is canceled, or failed, the user is informed. The callback is never called. */ public class DownloadExternalFile { private static final Log LOGGER = LogFactory.getLog(DownloadExternalFile.class); private final JabRefFrame frame; private final BibDatabaseContext databaseContext; private final BibEntry entry; private FileListEntryEditor editor; private boolean downloadFinished; private boolean dontShowDialog; public DownloadExternalFile(JabRefFrame frame, BibDatabaseContext databaseContext, BibEntry entry) { this.frame = frame; this.databaseContext = databaseContext; this.entry = entry; } /** * Look for the last '.' in the link, and return the following characters. * This gives the extension for most reasonably named links. * * @param link The link * @return The suffix, excluding the dot (e.g. "pdf") */ public static String getSuffix(final String link) { String strippedLink = link; try { // Try to strip the query string, if any, to get the correct suffix: URL url = new URL(link); if ((url.getQuery() != null) && (url.getQuery().length() < (link.length() - 1))) { strippedLink = link.substring(0, link.length() - url.getQuery().length() - 1); } } catch (MalformedURLException e) { // Don't report this error, since this getting the suffix is a non-critical // operation, and this error will be triggered and reported elsewhere. } // First see if the stripped link gives a reasonable suffix: String suffix; int strippedLinkIndex = strippedLink.lastIndexOf('.'); if ((strippedLinkIndex <= 0) || (strippedLinkIndex == (strippedLink.length() - 1))) { suffix = null; } else { suffix = strippedLink.substring(strippedLinkIndex + 1); } if (!ExternalFileTypes.getInstance().isExternalFileTypeByExt(suffix)) { // If the suffix doesn't seem to give any reasonable file type, try // with the non-stripped link: int index = link.lastIndexOf('.'); if ((index <= 0) || (index == (link.length() - 1))) { // No occurrence, or at the end // Check if there are path separators in the suffix - if so, it is definitely // not a proper suffix, so we should give up: if (strippedLink.substring(strippedLinkIndex + 1).indexOf('/') >= 1) { return ""; } else { return suffix; // return the first one we found, anyway. } } else { // Check if there are path separators in the suffix - if so, it is definitely // not a proper suffix, so we should give up: if (link.substring(index + 1).indexOf('/') >= 1) { return ""; } else { return link.substring(index + 1); } } } else { return suffix; } } /** * Start a download. * * @param callback The object to which the filename should be reported when download * is complete. */ public void download(final DownloadCallback callback) throws IOException { dontShowDialog = false; final String res = JOptionPane.showInputDialog(frame, Localization.lang("Enter URL to download")); if ((res == null) || res.trim().isEmpty()) { return; } URL url; try { url = new URL(res); } catch (MalformedURLException ex1) { JOptionPane.showMessageDialog(frame, Localization.lang("Invalid URL"), Localization.lang("Download file"), JOptionPane.ERROR_MESSAGE); return; } download(url, callback); } /** * Start a download. * * @param callback The object to which the filename should be reported when download * is complete. */ public void download(URL url, final DownloadCallback callback) throws IOException { String res = url.toString(); String mimeType; // First of all, start the download itself in the background to a temporary file: final File tmp = File.createTempFile("jabref_download", "tmp"); tmp.deleteOnExit(); URLDownload udl = new URLDownload(url); try { // TODO: what if this takes long time? // TODO: stop editor dialog if this results in an error: mimeType = udl.getMimeType(); // Read MIME type } catch (IOException ex) { JOptionPane.showMessageDialog(frame, Localization.lang("Invalid URL") + ": " + ex.getMessage(), Localization.lang("Download file"), JOptionPane.ERROR_MESSAGE); LOGGER.info("Error while downloading " + "'" + res + "'", ex); return; } final URL urlF = url; final URLDownload udlF = udl; JabRefExecutorService.INSTANCE.execute(() -> { try { udlF.toFile(tmp.toPath()); } catch (IOException e2) { dontShowDialog = true; if ((editor != null) && editor.isVisible()) { editor.setVisible(false, false); } JOptionPane.showMessageDialog(frame, Localization.lang("Invalid URL") + ": " + e2.getMessage(), Localization.lang("Download file"), JOptionPane.ERROR_MESSAGE); LOGGER.info("Error while downloading " + "'" + urlF + "'", e2); return; } // Download finished: call the method that stops the progress bar etc.: SwingUtilities.invokeLater(DownloadExternalFile.this::downloadFinished); }); Optional<ExternalFileType> suggestedType = Optional.empty(); if (mimeType != null) { LOGGER.debug("MIME Type suggested: " + mimeType); suggestedType = ExternalFileTypes.getInstance().getExternalFileTypeByMimeType(mimeType); } // Then, while the download is proceeding, let the user choose the details of the file: String suffix; if (suggestedType.isPresent()) { suffix = suggestedType.get().getExtension(); } else { // If we did not find a file type from the MIME type, try based on extension: suffix = getSuffix(res); if (suffix == null) { suffix = ""; } suggestedType = ExternalFileTypes.getInstance().getExternalFileTypeByExt(suffix); } String suggestedName = getSuggestedFileName(suffix); List<String> fDirectory = databaseContext.getFileDirectories(Globals.prefs.getFileDirectoryPreferences()); String directory; if (fDirectory.isEmpty()) { directory = null; } else { directory = fDirectory.get(0); } final String suggestDir = directory == null ? System.getProperty("user.home") : directory; File file = new File(new File(suggestDir), suggestedName); FileListEntry fileListEntry = new FileListEntry("", file.getCanonicalPath(), suggestedType); editor = new FileListEntryEditor(frame, fileListEntry, true, false, databaseContext, true); editor.getProgressBar().setIndeterminate(true); editor.setOkEnabled(false); editor.setExternalConfirm(closeEntry -> { File f = directory == null ? new File(closeEntry.getLink()) : expandFilename(directory, closeEntry.getLink()); if (f.isDirectory()) { JOptionPane.showMessageDialog(frame, Localization.lang("Target file cannot be a directory."), Localization.lang("Download file"), JOptionPane.ERROR_MESSAGE); return false; } if (f.exists()) { return JOptionPane.showConfirmDialog(frame, Localization.lang("'%0' exists. Overwrite file?", f.getName()), Localization.lang("Download file"), JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION; } else { return true; } }); if (dontShowDialog) { return; } else { editor.setVisible(true, false); } // Editor closed. Go on: if (editor.okPressed()) { File toFile = directory == null ? new File(fileListEntry.getLink()) : expandFilename(directory, fileListEntry.getLink()); String dirPrefix; if (directory == null) { dirPrefix = null; } else { if (directory.endsWith(OS.FILE_SEPARATOR)) { dirPrefix = directory; } else { dirPrefix = directory + OS.FILE_SEPARATOR; } } boolean success = FileUtil.copyFile(Paths.get(tmp.toURI()), Paths.get(toFile.toURI()), true); if (!success) { // OOps, the file exists! LOGGER.error("File already exists! DownloadExternalFile.download()"); } // If the local file is in or below the main file directory, change the // path to relative: if ((dirPrefix != null) && fileListEntry.getLink().startsWith(directory) && (fileListEntry.getLink().length() > dirPrefix.length())) { fileListEntry = new FileListEntry(fileListEntry.getDescription(), fileListEntry.getLink().substring(dirPrefix.length()), fileListEntry.getType()); } callback.downloadComplete(fileListEntry); if (!tmp.delete()) { LOGGER.info("Cannot delete temporary file"); } } else { // Canceled. Just delete the temp file: if (downloadFinished && !tmp.delete()) { LOGGER.info("Cannot delete temporary file"); } } } /** * Construct a File object pointing to the file linked, whether the link is * absolute or relative to the main directory. * * @param directory The main directory. * @param link The absolute or relative link. * @return The expanded File. */ private File expandFilename(String directory, String link) { File toFile = new File(link); // If this is a relative link, we should perhaps append the directory: String dirPrefix = directory + OS.FILE_SEPARATOR; if (!toFile.isAbsolute()) { toFile = new File(dirPrefix + link); } return toFile; } /** * This is called by the download thread when download is completed. */ private void downloadFinished() { downloadFinished = true; editor.getProgressBar().setVisible(false); editor.getProgressBarLabel().setVisible(false); editor.setOkEnabled(true); editor.getProgressBar().setValue(editor.getProgressBar().getMaximum()); } private String getSuggestedFileName(String suffix) { String plannedName = FileUtil.createFileNameFromPattern(databaseContext.getDatabase(), entry, Globals.prefs.get(JabRefPreferences.IMPORT_FILENAMEPATTERN), Globals.prefs.getLayoutFormatterPreferences(Globals.journalAbbreviationLoader)); 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; } /** * Callback interface that users of this class must implement in order to receive * notification when download is complete. */ @FunctionalInterface public interface DownloadCallback { void downloadComplete(FileListEntry file); } }