package org.jabref.gui.groups; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.dnd.DnDConstants; import java.awt.event.InputEvent; import java.awt.event.MouseEvent; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import javax.swing.JComponent; import javax.swing.JTable; import javax.swing.TransferHandler; import org.jabref.JabRefExecutorService; import org.jabref.gui.BasePanel; import org.jabref.gui.JabRefFrame; import org.jabref.gui.externalfiles.DroppedFileHandler; import org.jabref.gui.externalfiles.TransferableFileLinkSelection; import org.jabref.gui.externalfiletype.ExternalFileType; import org.jabref.gui.externalfiletype.ExternalFileTypes; import org.jabref.gui.importer.ImportMenuItem; import org.jabref.gui.importer.actions.OpenDatabaseAction; import org.jabref.gui.maintable.MainTable; import org.jabref.logic.net.URLDownload; import org.jabref.model.util.FileHelper; import org.jabref.pdfimport.PdfImporter; import org.jabref.pdfimport.PdfImporter.ImportPdfFilesResult; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class EntryTableTransferHandler extends TransferHandler { private static final boolean DROP_ALLOWED = true; private static final Log LOGGER = LogFactory.getLog(EntryTableTransferHandler.class); private final MainTable entryTable; private final JabRefFrame frame; private final BasePanel panel; private DataFlavor urlFlavor; private final DataFlavor stringFlavor; private boolean draggingFile; /** * Construct the transfer handler. * * @param entryTable The table this transfer handler should operate on. This argument is allowed to equal null, in * which case the transfer handler can assume that it works for a JabRef instance with no databases open, * attached to the empty tabbed pane. * @param frame The JabRefFrame instance. * @param panel The BasePanel this transferhandler works for. */ public EntryTableTransferHandler(MainTable entryTable, JabRefFrame frame, BasePanel panel) { this.entryTable = entryTable; this.frame = frame; this.panel = panel; stringFlavor = DataFlavor.stringFlavor; try { urlFlavor = new DataFlavor("application/x-java-url; class=java.net.URL"); } catch (ClassNotFoundException e) { LOGGER.info("Unable to configure drag and drop for main table", e); } } /** * Overridden to indicate which types of drags are supported (only LINK). */ @Override public int getSourceActions(JComponent c) { return DnDConstants.ACTION_LINK; } /** * This method is called when dragging stuff *from* the table. */ @Override public Transferable createTransferable(JComponent c) { if (draggingFile) { draggingFile = false; return new TransferableFileLinkSelection(panel, entryTable.getSelectedEntries());//.getTransferable(); } else { /* so we can assume it will never be called if entryTable==null: */ return new TransferableEntrySelection(entryTable.getSelectedEntries()); } } /** * This method is called when stuff is drag to the component. * * Imports the dropped URL or plain text as a new entry in the current library. * */ @Override public boolean importData(JComponent comp, Transferable t) { // If the drop target is the main table, we want to record which // row the item was dropped on, to identify the entry if needed: int dropRow = -1; if (comp instanceof JTable) { dropRow = ((JTable) comp).getSelectedRow(); } try { // This flavor is used for dragged file links in Windows: if (t.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { // JOptionPane.showMessageDialog(null, "Received // javaFileListFlavor"); @SuppressWarnings("unchecked") List<Path> files = ((List<File>) t.getTransferData(DataFlavor.javaFileListFlavor)).stream() .map(File::toPath).collect(Collectors.toList()); return handleDraggedFiles(files, dropRow); } else if (t.isDataFlavorSupported(urlFlavor)) { URL dropLink = (URL) t.getTransferData(urlFlavor); return handleDropTransfer(dropLink); } else if (t.isDataFlavorSupported(stringFlavor)) { String dropStr = (String) t.getTransferData(stringFlavor); LOGGER.debug("Received stringFlavor: " + dropStr); return handleDropTransfer(dropStr, dropRow); } } catch (IOException ioe) { LOGGER.error("Failed to read dropped data", ioe); } catch (UnsupportedFlavorException | ClassCastException ufe) { LOGGER.error("Drop type error", ufe); } // all supported flavors failed LOGGER.info("Can't transfer input: "); DataFlavor[] inflavs = t.getTransferDataFlavors(); for (DataFlavor inflav : inflavs) { LOGGER.info(" " + inflav); } return false; } /** * This method is called to query whether the transfer can be imported. * * Will return true for urls, strings, javaFileLists */ @Override public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { if (!EntryTableTransferHandler.DROP_ALLOWED) { return false; } // accept this if any input flavor matches any of our supported flavors for (DataFlavor inflav : transferFlavors) { if (inflav.match(urlFlavor) || inflav.match(stringFlavor) || inflav.match(DataFlavor.javaFileListFlavor)) { return true; } } // System.out.println("drop type forbidden"); // nope, never heard of this type return false; } @Override public void exportAsDrag(JComponent comp, InputEvent e, int action) { if (e instanceof MouseEvent) { int columnIndex = entryTable.columnAtPoint(((MouseEvent) e).getPoint()); int modelIndex = entryTable.getColumnModel().getColumn(columnIndex).getModelIndex(); if (entryTable.isFileColumn(modelIndex)) { LOGGER.info("Dragging file"); draggingFile = true; } } super.exportAsDrag(comp, e, DnDConstants.ACTION_LINK); } @Override protected void exportDone(JComponent source, Transferable data, int action) { // default implementation is OK super.exportDone(source, data, action); } @Override public void exportToClipboard(JComponent comp, Clipboard clip, int action) { // default implementation is OK super.exportToClipboard(comp, clip, action); } // add-ons ----------------------- private boolean handleDropTransfer(String dropStr, final int dropRow) throws IOException { if (dropStr.startsWith("file:")) { // This appears to be a dragged file link and not a reference // format. Check if we can map this to a set of files: if (handleDraggedFilenames(dropStr, dropRow)) { return true; // If not, handle it in the normal way... } } else if (dropStr.startsWith("http:")) { // This is the way URL links are received on OS X and KDE (Gnome?): URL url = new URL(dropStr); // JOptionPane.showMessageDialog(null, "Making URL: // "+url.toString()); return handleDropTransfer(url); } File tmpfile = File.createTempFile("jabrefimport", ""); tmpfile.deleteOnExit(); try (FileWriter fw = new FileWriter(tmpfile)) { fw.write(dropStr); } // System.out.println("importing from " + tmpfile.getAbsolutePath()); ImportMenuItem importer = new ImportMenuItem(frame, false); importer.automatedImport(Collections.singletonList(tmpfile.getAbsolutePath())); return true; } /** * Translate a String describing a set of files or URLs dragged into JabRef into a List of File objects, taking care * of URL special characters. * * @param s String describing a set of files or URLs dragged into JabRef * @return a List<File> containing the individual file objects. * */ public static List<Path> getFilesFromDraggedFilesString(String s) { // Split into lines: String[] lines = s.replace("\r", "").split("\n"); List<Path> files = new ArrayList<>(); for (String line1 : lines) { String line = line1; // Try to use url.toURI() to translate URL specific sequences like %20 into // standard characters: File fl = null; try { URL url = new URL(line); fl = new File(url.toURI()); } catch (MalformedURLException | URISyntaxException e) { LOGGER.warn("Could not get file", e); } // Unless an exception was thrown, we should have the sanitized path: if (fl != null) { line = fl.getPath(); } else if (line.startsWith("file:")) { line = line.substring(5); } else { continue; } // Under Gnome, the link is given as file:///...., so we // need to strip the extra slashes: if (line.startsWith("//")) { line = line.substring(2); } File f = new File(line); if (f.exists()) { files.add(f.toPath()); } } return files; } /** * Handle a String describing a set of files or URLs dragged into JabRef. * * @param s String describing a set of files or URLs dragged into JabRef * @param dropRow The row in the table where the files were dragged. * @return success status for the operation * */ private boolean handleDraggedFilenames(String s, final int dropRow) { return handleDraggedFiles(EntryTableTransferHandler.getFilesFromDraggedFilesString(s), dropRow); } /** * Handle a List containing File objects for a set of files to import. * * @param files A List containing File instances pointing to files. * @param dropRow @param dropRow The row in the table where the files were dragged. * @return success status for the operation */ private boolean handleDraggedFiles(List<Path> files, final int dropRow) { final List<String> fileNames = new ArrayList<>(); for (Path file : files) { fileNames.add(file.toAbsolutePath().toString()); } // Try to load BIB files normally, and import the rest into the current // database. // This process must be spun off into a background thread: JabRefExecutorService.INSTANCE.execute(() -> { final ImportPdfFilesResult importRes = new PdfImporter(frame, panel, entryTable, dropRow) .importPdfFiles(fileNames); if (!importRes.getNoPdfFiles().isEmpty()) { loadOrImportFiles(importRes.getNoPdfFiles(), dropRow); } }); return true; } /** * Take a set of filenames. Those with names indicating BIB files are opened as such if possible. All other files we * will attempt to import into the current library. * * @param fileNames The names of the files to open. * @param dropRow success status for the operation */ private void loadOrImportFiles(List<String> fileNames, int dropRow) { OpenDatabaseAction openAction = new OpenDatabaseAction(frame, false); List<String> notBibFiles = new ArrayList<>(); List<String> bibFiles = new ArrayList<>(); for (String fileName : fileNames) { // Find the file's extension, if any: Optional<String> extension = FileHelper.getFileExtension(fileName); Optional<ExternalFileType> fileType; if (extension.isPresent() && "bib".equals(extension.get())) { // we assume that it is a BibTeX file. // When a user wants to import something with file extension "bib", but which is not a BibTeX file, he should use "file -> import" bibFiles.add(fileName); continue; } fileType = ExternalFileTypes.getInstance().getExternalFileTypeByExt(extension.orElse("")); /* * This is a linkable file. If the user dropped it on an entry, we * should offer options for autolinking to this files: * * TODO we should offer an option to highlight the row the user is on too. */ if ((fileType.isPresent()) && (dropRow >= 0)) { /* * TODO: make this an instance variable? */ DroppedFileHandler dfh = new DroppedFileHandler(frame, panel); dfh.handleDroppedfile(fileName, fileType.get(), entryTable, dropRow); continue; } notBibFiles.add(fileName); } openAction.openFilesAsStringList(bibFiles, true); if (!notBibFiles.isEmpty()) { // Import into new if entryTable==null, otherwise into current // database: ImportMenuItem importer = new ImportMenuItem(frame, entryTable == null); importer.automatedImport(notBibFiles); } } private boolean handleDropTransfer(URL dropLink) throws IOException { File tmpfile = File.createTempFile("jabrefimport", ""); tmpfile.deleteOnExit(); new URLDownload(dropLink).toFile(tmpfile.toPath()); // Import into new if entryTable==null, otherwise into current library: ImportMenuItem importer = new ImportMenuItem(frame, entryTable == null); importer.automatedImport(Collections.singletonList(tmpfile.getAbsolutePath())); return true; } }