package org.jabref.gui.desktop; import java.io.File; import java.io.IOException; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.regex.Pattern; import javax.swing.JOptionPane; import org.jabref.Globals; import org.jabref.JabRefGUI; import org.jabref.gui.ClipBoardManager; import org.jabref.gui.IconTheme; import org.jabref.gui.JabRefFrame; import org.jabref.gui.desktop.os.DefaultDesktop; import org.jabref.gui.desktop.os.Linux; import org.jabref.gui.desktop.os.NativeDesktop; import org.jabref.gui.desktop.os.OSX; import org.jabref.gui.desktop.os.Windows; import org.jabref.gui.externalfiletype.ExternalFileType; import org.jabref.gui.externalfiletype.ExternalFileTypeEntryEditor; import org.jabref.gui.externalfiletype.ExternalFileTypes; import org.jabref.gui.externalfiletype.UnknownExternalFileType; import org.jabref.gui.filelist.FileListEntry; import org.jabref.gui.filelist.FileListEntryEditor; import org.jabref.gui.filelist.FileListTableModel; import org.jabref.gui.undo.UndoableFieldChange; import org.jabref.logic.l10n.Localization; import org.jabref.logic.util.OS; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.FieldName; import org.jabref.model.entry.identifier.DOI; import org.jabref.model.entry.identifier.Eprint; import org.jabref.model.util.FileHelper; import org.jabref.preferences.JabRefPreferences; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * TODO: Replace by http://docs.oracle.com/javase/7/docs/api/java/awt/Desktop.html * http://stackoverflow.com/questions/18004150/desktop-api-is-not-supported-on-the-current-platform */ public class JabRefDesktop { private static final Log LOGGER = LogFactory.getLog(JabRefDesktop.class); private static final NativeDesktop NATIVE_DESKTOP = getNativeDesktop(); private static final Pattern REMOTE_LINK_PATTERN = Pattern.compile("[a-z]+://.*"); private JabRefDesktop() { } /** * Open a http/pdf/ps viewer for the given link string. */ public static void openExternalViewer(BibDatabaseContext databaseContext, String initialLink, String initialFieldName) throws IOException { String link = initialLink; String fieldName = initialFieldName; if (FieldName.PS.equals(fieldName) || FieldName.PDF.equals(fieldName)) { // Find the default directory for this field type: List<String> dir = databaseContext.getFileDirectories(fieldName, Globals.prefs.getFileDirectoryPreferences()); Optional<Path> file = FileHelper.expandFilename(link, dir); // Check that the file exists: if (!file.isPresent() || !Files.exists(file.get())) { throw new IOException("File not found (" + fieldName + "): '" + link + "'."); } link = file.get().toAbsolutePath().toString(); // Use the correct viewer even if pdf and ps are mixed up: String[] split = file.get().getFileName().toString().split("\\."); if (split.length >= 2) { if ("pdf".equalsIgnoreCase(split[split.length - 1])) { fieldName = FieldName.PDF; } else if ("ps".equalsIgnoreCase(split[split.length - 1]) || ((split.length >= 3) && "ps".equalsIgnoreCase(split[split.length - 2]))) { fieldName = FieldName.PS; } } } else if (FieldName.DOI.equals(fieldName)) { openDoi(link); return; } else if (FieldName.EPRINT.equals(fieldName)) { link = Eprint.build(link).map(Eprint::getURIAsASCIIString).orElse(link); // should be opened in browser fieldName = FieldName.URL; } if (FieldName.URL.equals(fieldName)) { openBrowser(link); } else if (FieldName.PS.equals(fieldName)) { try { NATIVE_DESKTOP.openFile(link, FieldName.PS); } catch (IOException e) { LOGGER.error("An error occured on the command: " + link, e); } } else if (FieldName.PDF.equals(fieldName)) { try { NATIVE_DESKTOP.openFile(link, FieldName.PDF); } catch (IOException e) { LOGGER.error("An error occured on the command: " + link, e); } } else { LOGGER.info("Message: currently only PDF, PS and HTML files can be opened by double clicking"); } } private static void openDoi(String doi) throws IOException { String link = DOI.parse(doi).map(DOI::getURIAsASCIIString).orElse(doi); openBrowser(link); } /** * Open an external file, attempting to use the correct viewer for it. * * @param databaseContext * The database this file belongs to. * @param link * The filename. * @return false if the link couldn't be resolved, true otherwise. */ public static boolean openExternalFileAnyFormat(final BibDatabaseContext databaseContext, String link, final Optional<ExternalFileType> type) throws IOException { boolean httpLink = false; if (REMOTE_LINK_PATTERN.matcher(link.toLowerCase(Locale.ROOT)).matches()) { httpLink = true; } // For other platforms we'll try to find the file type: Path file = Paths.get(link); if (!httpLink) { Optional<Path> tmp = FileHelper.expandFilename(databaseContext, link, Globals.prefs.getFileDirectoryPreferences()); if (tmp.isPresent()) { file = tmp.get(); } } // Check if we have arrived at a file type, and either an http link or an existing file: if (httpLink || Files.exists(file) && (type.isPresent())) { // Open the file: String filePath = httpLink ? link : file.toString(); openExternalFilePlatformIndependent(type, filePath); return true; } else { // No file matched the name, or we did not know the file type. return false; } } public static boolean openExternalFileAnyFormat(Path file, final BibDatabaseContext databaseContext, final Optional<ExternalFileType> type) throws IOException { return openExternalFileAnyFormat(databaseContext, file.toString(), type); } private static void openExternalFilePlatformIndependent(Optional<ExternalFileType> fileType, String filePath) throws IOException { if (fileType.isPresent()) { String application = fileType.get().getOpenWithApplication(); if (application.isEmpty()) { NATIVE_DESKTOP.openFile(filePath, fileType.get().getExtension()); } else { NATIVE_DESKTOP.openFileWithApplication(filePath, application); } } } public static boolean openExternalFileUnknown(JabRefFrame frame, BibEntry entry, BibDatabaseContext databaseContext, String link, UnknownExternalFileType fileType) throws IOException { String cancelMessage = Localization.lang("Unable to open file."); String[] options = new String[] {Localization.lang("Define '%0'", fileType.getName()), Localization.lang("Change file type"), Localization.lang("Cancel")}; String defOption = options[0]; int answer = JOptionPane.showOptionDialog(frame, Localization.lang("This external link is of the type '%0', which is undefined. What do you want to do?", fileType.getName()), Localization.lang("Undefined file type"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, defOption); if (answer == JOptionPane.CANCEL_OPTION) { frame.output(cancelMessage); return false; } else if (answer == JOptionPane.YES_OPTION) { // User wants to define the new file type. Show the dialog: ExternalFileType newType = new ExternalFileType(fileType.getName(), "", "", "", "new", IconTheme.JabRefIcon.FILE.getSmallIcon()); ExternalFileTypeEntryEditor editor = new ExternalFileTypeEntryEditor(frame, newType); editor.setVisible(true); if (editor.okPressed()) { // Get the old list of types, add this one, and update the list in prefs: List<ExternalFileType> fileTypes = new ArrayList<>( ExternalFileTypes.getInstance().getExternalFileTypeSelection()); fileTypes.add(newType); Collections.sort(fileTypes); ExternalFileTypes.getInstance().setExternalFileTypes(fileTypes); // Finally, open the file: return openExternalFileAnyFormat(databaseContext, link, Optional.of(newType)); } else { // Canceled: frame.output(cancelMessage); return false; } } else { // User wants to change the type of this link. // First get a model of all file links for this entry: FileListTableModel tModel = new FileListTableModel(); Optional<String> oldValue = entry.getField(FieldName.FILE); oldValue.ifPresent(tModel::setContent); FileListEntry flEntry = null; // Then find which one we are looking at: for (int i = 0; i < tModel.getRowCount(); i++) { FileListEntry iEntry = tModel.getEntry(i); if (iEntry.getLink().equals(link)) { flEntry = iEntry; break; } } if (flEntry == null) { // This shouldn't happen, so I'm not sure what to put in here: throw new RuntimeException("Could not find the file list entry " + link + " in " + entry); } FileListEntryEditor editor = new FileListEntryEditor(frame, flEntry, false, true, databaseContext); editor.setVisible(true, false); if (editor.okPressed()) { // Store the changes and add an undo edit: String newValue = tModel.getStringRepresentation(); UndoableFieldChange ce = new UndoableFieldChange(entry, FieldName.FILE, oldValue.orElse(null), newValue); entry.setField(FieldName.FILE, newValue); frame.getCurrentBasePanel().getUndoManager().addEdit(ce); frame.getCurrentBasePanel().markBaseChanged(); // Finally, open the link: return openExternalFileAnyFormat(databaseContext, flEntry.getLink(), flEntry.getType()); } else { // Canceled: frame.output(cancelMessage); return false; } } } /** * Opens a file browser of the folder of the given file. If possible, the file is selected * @param fileLink the location of the file * @throws IOException */ public static void openFolderAndSelectFile(Path fileLink) throws IOException { NATIVE_DESKTOP.openFolderAndSelectFile(fileLink); } /** * Opens the given URL using the system browser * * @param url the URL to open * @throws IOException */ public static void openBrowser(String url) throws IOException { Optional<ExternalFileType> fileType = ExternalFileTypes.getInstance().getExternalFileTypeByExt("html"); openExternalFilePlatformIndependent(fileType, url); } public static void openBrowser(URI url) throws IOException { openBrowser(url.toASCIIString()); } /** * Opens the url with the users standard Browser. * If that fails a popup will be shown to instruct the user to open the link manually * and the link gets copied to the clipboard * @param url */ public static void openBrowserShowPopup(String url) { try { openBrowser(url); } catch (IOException exception) { new ClipBoardManager().setClipboardContents(url); LOGGER.error("Could not open browser", exception); String couldNotOpenBrowser = Localization.lang("Could not open browser."); String openManually = Localization.lang("Please open %0 manually.", url); String copiedToClipboard = Localization.lang("The_link_has_been_copied_to_the_clipboard."); JabRefGUI.getMainFrame().output(couldNotOpenBrowser); JOptionPane.showMessageDialog(JabRefGUI.getMainFrame(), couldNotOpenBrowser + "\n" + openManually + "\n" + copiedToClipboard, couldNotOpenBrowser, JOptionPane.ERROR_MESSAGE); } } /** * Opens a new console starting on the given file location * * If no command is specified in {@link Globals}, * the default system console will be executed. * * @param file Location the console should be opened at. */ public static void openConsole(File file) throws IOException { if (file == null) { return; } String absolutePath = file.toPath().toAbsolutePath().getParent().toString(); boolean usingDefault = Globals.prefs.getBoolean(JabRefPreferences.USE_DEFAULT_CONSOLE_APPLICATION); if (usingDefault) { NATIVE_DESKTOP.openConsole(absolutePath); } else { String command = Globals.prefs.get(JabRefPreferences.CONSOLE_COMMAND); command = command.trim(); if (!command.isEmpty()) { command = command.replaceAll("\\s+", " "); // normalize white spaces String[] subcommands = command.split(" "); // replace the placeholder if used String commandLoggingText = command.replace("%DIR", absolutePath); JabRefGUI.getMainFrame().output(Localization.lang("Executing command \"%0\"...", commandLoggingText)); LOGGER.info("Executing command \"" + commandLoggingText + "\"..."); try { new ProcessBuilder(subcommands).start(); } catch (IOException exception) { LOGGER.error("Open console", exception); JOptionPane.showMessageDialog(JabRefGUI.getMainFrame(), Localization.lang("Error_occured_while_executing_the_command_\"%0\".", commandLoggingText), Localization.lang("Open console") + " - " + Localization.lang("Error"), JOptionPane.ERROR_MESSAGE); JabRefGUI.getMainFrame().output(null); } } } } // TODO: Move to OS.java public static NativeDesktop getNativeDesktop() { if (OS.WINDOWS) { return new Windows(); } else if (OS.OS_X) { return new OSX(); } else if (OS.LINUX) { return new Linux(); } return new DefaultDesktop(); } }