/* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2016 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.mucommander.ui.dnd; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.protocol.FileProtocols; import com.mucommander.commons.file.util.FileSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.util.List; import java.util.Vector; /** * This class represents a Transferable file set and is used for Drag and Drop transfers initiated by muCommander * (dragged from a muCommander UI component). * * <p>The actual file set data can be fetched using one of those 3 DataFlavors : * <ul> * <li>FileSetDataFlavor (as returned by {@link #getFileSetDataFlavor()}): data returned as a {@link com.mucommander.commons.file.util.FileSet}. * This flavor is used for local file transfers (within the application) only. In particular, this DataFlavor cannot * be used to transfer data to the clipboard because the data (FileSet) cannot be serialized. * In this case, the {@link #setFileSetDataFlavorSupported(boolean)} method should be used to disable FileSet DataFlavor. * </li> * <li>DataFlavor.javaFileListFlavor : data returned as a java.util.Vector of <code>java.io.File</code> files. * This flavor is used for file transfers to and from external applications. * </li> * <li>text/uri-list (RFC 2483): an alternative flavor supported by Gnome and KDE where the data is returned as * a String containing file URIs separated by '\r\n'. This flavor is as of today the only one supported by GNOME and * KDE. See bug #45 and http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4899516 . * </li> * <li>DataFlavor.stringFlavor: data returned as a String containing files paths separated by \n characters. * This other alternative flavor is used for file transfers to and from external applications that do not support * either of DataFlavor.javaFileListFlavor and text/uri-list but text only (plain text editors for example). * </li> * </ul> * * @author Maxence Bernard, Xavi MirĂ³ */ public class TransferableFileSet implements Transferable { private static final Logger LOGGER = LoggerFactory.getLogger(TransferableFileSet.class); /** Transferred FileSet */ private FileSet fileSet; /** Is FileSet DataFlavor supported ? */ private boolean fileSetFlavorSupported = true; /** Is DataFlavor.javaFileListFlavor supported ? */ private boolean javaFileListFlavorSupported = true; /** Is DataFlavor.stringFlavor supported ? */ private boolean stringFlavorSupported = true; /** Is text/uri-list (RFC 2483) flavor supported ? */ private boolean textUriFlavorSupported = true; /** Does DataFlavor.stringFlavor transfer the files' full paths or filenames only ? */ private boolean stringFlavourTransfersFilename = false; /** Does DataFlavor.stringFlavor transfer the files' filenames with extension or without ? */ private boolean stringFlavourTransfersFileBaseName = false; /** DataFlavor used for GNOME/KDE transfers */ private static DataFlavor TEXT_URI_FLAVOR; /** Custom FileSet DataFlavor used for local transfers */ private static DataFlavor FILE_SET_DATA_FLAVOR; static { // Create a single custom DataFlavor instance that designates the FileSet class to transfer data try { FILE_SET_DATA_FLAVOR = new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType+";class="+FileSet.class.getName()); TEXT_URI_FLAVOR = new DataFlavor("text/uri-list;class="+String.class.getName()); } catch(ClassNotFoundException e) { // That should never happen LOGGER.debug("FileSet DataFlavor could not be instantiated", e); } } /** * Creates a new Transferable file set with support for all DataFlavors enabled. * * @param fileSet the files to be transferred */ public TransferableFileSet(FileSet fileSet) { this.fileSet = fileSet; } /** * Sets whether or not the FileSet <code>DataFlavor</code> (as returned by {@link #getFileSetDataFlavor()} * should be supported by this Transferable (supported by default). * * @param supported <code>true</code> to support the flavor */ public void setFileSetDataFlavorSupported(boolean supported) { this.fileSetFlavorSupported = supported; } /** * Sets whether or not the <code>DataFlavor.javaFileListFlavor</code> should be supported by this Transferable * (supported by default). * * @param supported <code>true</code> to support the flavor */ public void setJavaFileListDataFlavorSupported(boolean supported) { this.javaFileListFlavorSupported = supported; } /** * Sets whether or not the <code>DataFlavor.stringFlavor</code> should be supported by this Transferable * (supported by default). * * @param supported <code>true</code> to support the flavor */ public void setStringDataFlavorSupported(boolean supported) { this.stringFlavorSupported = supported; } /** * Sets whether or not the <code>text/uri-list</code> (RFC 2483) should be supported by this Transferable * (supported by default). * * @param supported <code>true</code> to support the flavor */ public void setTextUriFlavorSupported(boolean supported) { this.textUriFlavorSupported = supported; } /** * Sets whether the files' full path or just the filenames should be returned when * {@link #getTransferData(java.awt.datatransfer.DataFlavor)} is called with <code>DataFlavor.stringFlavor</code>. * (*not* enabled by default) * * @param b if <code>true</code>, DataFlavor.stringFlavor returns filenames only, full file paths otherwise. */ public void setStringDataFlavourTransfersFilename(boolean b) { this.stringFlavourTransfersFilename = b; } /** * Returns whether the files' full path or just the filenames will be returned when * {@link #getTransferData(java.awt.datatransfer.DataFlavor)} is called with <code>DataFlavor.stringFlavor</code>. * Returns <code>false</code> unless {@link #setStringDataFlavourTransfersFilename(boolean)} has been called. * * @return whether the files' full path or just the filenames will be returned when * {@link #getTransferData(java.awt.datatransfer.DataFlavor)} is called with <code>DataFlavor.stringFlavor</code> */ public boolean getStringDataFlavourTransfersFilename() { return this.stringFlavourTransfersFilename; } /** * Sets whether the files' base name (without file extension) should be returned when * {@link #getTransferData(java.awt.datatransfer.DataFlavor)} is called with <code>DataFlavor.stringFlavor</code>. * (*not* enabled by default) * * @param b if <code>true</code>, DataFlavor.stringFlavor returns filenames without extension, full file name otherwise. */ public void setStringDataFlavourTransfersFileBaseName(boolean b) { this.stringFlavourTransfersFileBaseName = b; } /** * Returns whether the files' base name (without file extension) should be returned when * {@link #getTransferData(java.awt.datatransfer.DataFlavor)} is called with <code>DataFlavor.stringFlavor</code>. * Returns <code>false</code> unless {@link #setStringDataFlavourTransfersFileBaseName(boolean)} has been called. * * @return whether the files' base name (without file extension) should be returned when * {@link #getTransferData(java.awt.datatransfer.DataFlavor)} is called with <code>DataFlavor.stringFlavor</code> */ public boolean getStringDataFlavourTransfersFileBaseName() { return stringFlavourTransfersFileBaseName; } /** * Returns an instance of the custom FileSet DataFlavor used to transfer files locally. * * @return an instance of the custom FileSet DataFlavor used to transfer files locally */ public static DataFlavor getFileSetDataFlavor() { return FILE_SET_DATA_FLAVOR; } /** * Returns the files contained by the specified Transferable as a {@link com.mucommander.commons.file.util.FileSet}, * or <code>null</code> if no file was present in the Transferable or if an error occurred. * * <p>3 types of dropped data flavors are supported and used in this order of priority: * <ul> * <li>FileSet: the local DataFlavor used when files are transferred from muCommander * <li>File list: used when files are transferred from an external application * <li>File paths: alternate flavor used when some text representing one or several file paths is dragged * from an external application * </ul> * * @param transferable a Transferable instance that contains the files to be retrieved * @return the files contained by the specified Transferable as a FileSet, or <code>null</code> if no file * was present or if an error occurred */ public static FileSet getTransferFiles(Transferable transferable) { FileSet files; AbstractFile file; try { // FileSet DataFlavor if(transferable.isDataFlavorSupported(FILE_SET_DATA_FLAVOR)) { files = (FileSet)transferable.getTransferData(FILE_SET_DATA_FLAVOR); } // File list DataFlavor else if(transferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { List<File> fileList = (List<File>)transferable.getTransferData(DataFlavor.javaFileListFlavor); int nbFiles = fileList.size(); files = new FileSet(); for(int i=0; i<nbFiles; i++) { file = FileFactory.getFile(fileList.get(i).getAbsolutePath()); if(file!=null) files.add(file); } } // Text plain DataFlavor: assume that lines designate file paths else if(transferable.isDataFlavorSupported(DataFlavor.stringFlavor)) { BufferedReader br; br = null; try { br = new BufferedReader(DataFlavor.getTextPlainUnicodeFlavor().getReaderForText(transferable)); // Read input line by line and try to create AbstractFile instances String path; files = new FileSet(); while((path=br.readLine())!=null) { // Try to create an AbstractFile instance, returned instance may be null file = FileFactory.getFile(path); // Safety precaution: if at least one line doesn't resolve as a file, stop reading // and return null. This is to avoid any nasty effect that could arise if a random // piece of text (let's say an email contents) was inadvertently pasted or dropped to muCommander. if(file==null) return null; files.add(file); } } // Documentation is not explicit on whether DataFlavor streams need to be closed, we might as well // do so just to be sure. finally { if(br != null) { try {br.close();} catch(IOException e) {} } } } else { return null; } } catch(Exception e) { // Catch UnsupportedFlavorException, IOException LOGGER.debug("Caught exception while processing transferable", e); return null; } return files; } ////////////////////////// // Transferable methods // ////////////////////////// public DataFlavor[] getTransferDataFlavors() { List<DataFlavor> supportedDataFlavorsV = new Vector<DataFlavor>(); if(fileSetFlavorSupported) supportedDataFlavorsV.add(FILE_SET_DATA_FLAVOR); if(javaFileListFlavorSupported) supportedDataFlavorsV.add(DataFlavor.javaFileListFlavor); if(stringFlavorSupported) supportedDataFlavorsV.add(DataFlavor.stringFlavor); if(textUriFlavorSupported) supportedDataFlavorsV.add(TEXT_URI_FLAVOR); DataFlavor supportedDataFlavors[] = new DataFlavor[supportedDataFlavorsV.size()]; supportedDataFlavorsV.toArray(supportedDataFlavors); return supportedDataFlavors; } public boolean isDataFlavorSupported(DataFlavor dataFlavor) { if(dataFlavor.equals(FILE_SET_DATA_FLAVOR)) return fileSetFlavorSupported; else if(dataFlavor.equals(DataFlavor.javaFileListFlavor)) return javaFileListFlavorSupported; else if(dataFlavor.equals(DataFlavor.stringFlavor)) return stringFlavorSupported; else if(dataFlavor.equals(TEXT_URI_FLAVOR)) return textUriFlavorSupported; return false; } public Object getTransferData(DataFlavor dataFlavor) throws UnsupportedFlavorException, IOException { int nbFiles = fileSet.size(); // Return files stored in a FileSet instance (the one that was passed to the constructor) if(dataFlavor.equals(FILE_SET_DATA_FLAVOR) && fileSetFlavorSupported) { return fileSet; } // Return files stored in a java.util.Vector instance else if(dataFlavor.equals(DataFlavor.javaFileListFlavor) && javaFileListFlavorSupported) { List<File> fileList = new Vector<File>(nbFiles); for(int i=0; i<nbFiles; i++) { AbstractFile file = fileSet.elementAt(i); fileList.add(new File(file.getAbsolutePath())); } return fileList; } // // Return an InputStream formatted in a specified Unicode charset that contains file paths separated by '\n' characters // else if(dataFlavor.equals(DataFlavor.getTextPlainUnicodeFlavor())) { // String mimeType = dataFlavor.getMimeType(); // String charset = mimeType.substring(mimeType.indexOf("charset=")+8, mimeType.length()); // // StringBuilder sb = new StringBuilder(); // for(int i=0; i<nbFiles; i++) { // sb.append(fileSet.fileAt(i).getAbsolutePath()); // if(i!=nbFiles-1) // sb.append('\n'); // } // // return new ByteArrayInputStream(sb.toString().getBytes(charset)); // } // Return a String with file paths or names else if(dataFlavor.equals(DataFlavor.stringFlavor) && stringFlavorSupported) { StringBuilder sb = new StringBuilder(); AbstractFile file; for(int i=0; i<nbFiles; i++) { file = fileSet.elementAt(i); // Check if to return string with filenames if (stringFlavourTransfersFilename) { // Append just base name or file name with extension sb.append(stringFlavourTransfersFileBaseName ? file.getBaseName() : file.getName()); } else { sb.append(file.getAbsolutePath()); } if(i!=nbFiles-1) sb.append('\n'); } return sb.toString(); } // Return a String with file URLs, as specified by RFC 2483 else if(dataFlavor.equals(TEXT_URI_FLAVOR) && textUriFlavorSupported) { StringBuilder sb = new StringBuilder(); AbstractFile file; for(int i=0; i<nbFiles; i++) { file = fileSet.elementAt(i); String url = file.getURL().getScheme().equals(FileProtocols.FILE) // Use java.io.File.toURI() for local files (e.g. file:/mnt/music), this is the format expected by // Gnome/Nautilus. ?new File(file.getAbsolutePath()).toURI().toString() // Use standard URL format (e.g. smb://host/share/file) for other file types :file.getAbsolutePath(); sb.append(url); if(i!=nbFiles-1){ sb.append("\r\n"); } } return sb.toString(); } // Any other requested DataFlavor will thrown an UnsupportedFlavorException else { throw new UnsupportedFlavorException(dataFlavor); } } }