/* * VFS.java - Virtual filesystem implementation * :tabSize=4:indentSize=4:noTabs=false: * :folding=explicit:collapseFolds=1: * * Copyright (C) 2000, 2003 Slava Pestov * * This program 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 2 * of the License, or any later version. * * This program 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, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.gjt.sp.jedit.io; //{{{ Imports import java.awt.Color; import java.awt.Component; import java.io.*; import java.io.Closeable; import java.util.*; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import org.gjt.sp.jedit.msg.PropertiesChanged; import org.gjt.sp.jedit.*; import org.gjt.sp.jedit.bufferio.BufferLoadRequest; import org.gjt.sp.jedit.bufferio.BufferSaveRequest; import org.gjt.sp.jedit.bufferio.BufferInsertRequest; import org.gjt.sp.util.Log; import org.gjt.sp.util.ProgressObserver; import org.gjt.sp.util.IOUtilities; import org.gjt.sp.util.StandardUtilities; import org.gjt.sp.util.Task; import org.gjt.sp.util.ThreadUtilities; //}}} /** * A virtual filesystem implementation.<p> * * Plugins can provide virtual file systems by defining entries in their * <code>services.xml</code> files like so: * * <pre><SERVICE CLASS="org.gjt.sp.jedit.io.VFS" NAME="<i>name</i>"> * new <i>MyVFS</i>(); *</SERVICE></pre> * * URLs of the form <code><i>name</i>:<i>path</i></code> will then be handled * by the VFS named <code><i>name</i></code>.<p> * * See {@link org.gjt.sp.jedit.ServiceManager} for details.<p> * * <h3>Session objects:</h3> * * A session is used to persist things like login information, any network * sockets, etc. File system implementations that do not need this kind of * persistence return a dummy object as a session.<p> * * Methods whose names are prefixed with "_" expect to be given a * previously-obtained session object. A session must be obtained with * this method: * * <ul> * <li>{@link #createVFSSession(String,Component)}</li> * </ul> * * That method should be called from the AWT (EDT) thread, unless * the filesystem has <code>NON_AWT_SESSION_CAP</code> capability.<p> * * When done, the session must be disposed of using * {@link #_endVFSSession(Object,Component)}.<p> * * <h3>Thread safety:</h3> * * The following methods cannot be called from an I/O thread: * * <ul> * <li>{@link #createVFSSession(String,Component)} - unless * <code>NON_AWT_SESSION_CAP</code> capability is set</li> * <li>{@link #insert(View,Buffer,String)}</li> * <li>{@link #load(View,Buffer,String)}</li> * <li>{@link #save(View,Buffer,String)}</li> * </ul> * * All remaining methods are required to be thread-safe in subclasses. * * <h3>Implementing a VFS</h3> * * You can override as many or as few methods as you want. Make sure * {@link #getCapabilities()} returns a value reflecting the functionality * implemented by your VFS. * * @see VFSManager#getVFSForPath(String) * @see VFSManager#getVFSForProtocol(String) * * @author Slava Pestov * @author $Id$ */ public abstract class VFS { //{{{ Capabilities /** * Read capability. * @since jEdit 2.6pre2 */ public static final int READ_CAP = 1 << 0; /** * Write capability. * @since jEdit 2.6pre2 */ public static final int WRITE_CAP = 1 << 1; /** * Browse capability * @since jEdit 4.3pre11 * * This was the official API for adding items to a file * system browser's <b>Plugins</b> menu in jEdit 4.1 and earlier. In * jEdit 4.2, there is a different way of doing this, you must provide * a <code>browser.actions.xml</code> file in your plugin JAR, and * define <code>plugin.<i>class</i>.browser-menu-item</code> * or <code>plugin.<i>class</i>.browser-menu</code> properties. * See {@link org.gjt.sp.jedit.EditPlugin} for details. */ public static final int BROWSE_CAP = 1 << 2; /** * Delete file capability. * @since jEdit 2.6pre2 */ public static final int DELETE_CAP = 1 << 3; /** * Rename file capability. * @since jEdit 2.6pre2 */ public static final int RENAME_CAP = 1 << 4; /** * Make directory capability. * @since jEdit 2.6pre2 */ public static final int MKDIR_CAP = 1 << 5; /** * Low latency capability. If this is not set, then a confirm dialog * will be shown before doing a directory search in this VFS. * @since jEdit 4.1pre1 */ public static final int LOW_LATENCY_CAP = 1 << 6; /** * Case insensitive file system capability. * @since jEdit 4.1pre1 */ public static final int CASE_INSENSITIVE_CAP = 1 << 7; /** * Sessions created outside Event Dispatching Thread - * file system capability. Set for the file system that does not * require that <code>createVFSSession</code> is called on edt. * All systems that do not implement <code>createVFSSession</code> * should set it, but others may too. * @since jEdit 5.0pre1 */ public static final int NON_AWT_SESSION_CAP = 1 << 8; //}}} //{{{ Extended attributes /** * File type. * @since jEdit 4.2pre1 */ public static final String EA_TYPE = "type"; /** * File status (read only, read write, etc). * @since jEdit 4.2pre1 */ public static final String EA_STATUS = "status"; /** * File size. * @since jEdit 4.2pre1 */ public static final String EA_SIZE = "size"; /** * File last modified date. * @since jEdit 4.2pre1 */ public static final String EA_MODIFIED = "modified"; //}}} public static final int IOBUFSIZE = 32678; //{{{ VFS constructors /** * Creates a new virtual filesystem. * @param name The name * @param caps The capabilities */ protected VFS(String name, int caps) { this.name = name; this.caps = caps; // reasonable defaults (?) this.extAttrs = new String[] { EA_SIZE, EA_TYPE }; } /** * Creates a new virtual filesystem. * @param name The name * @param caps The capabilities * @param extAttrs The extended attributes * @since jEdit 4.2pre1 */ protected VFS(String name, int caps, String[] extAttrs) { this.name = name; this.caps = caps; this.extAttrs = extAttrs; } //}}} //{{{ getName() method /** * Returns this VFS's name. The name is used to obtain the * label stored in the <code>vfs.<i>name</i>.label</code> * property. */ public String getName() { return name; } //}}} //{{{ getCapabilities() method /** * Returns the capabilities of this VFS. * @since jEdit 2.6pre2 */ public int getCapabilities() { return caps; } //}}} //{{{ isMarkersFileSupported() method /** * Returns if an additional markers file can be saved by this VFS. * Default is {@code true}. * * @since jEdit 4.3pre10 */ public boolean isMarkersFileSupported() { return true; } //}}} //{{{ getExtendedAttributes() method /** * Returns the extended attributes supported by this VFS. * @since jEdit 4.2pre1 */ public String[] getExtendedAttributes() { return extAttrs; } //}}} //{{{ getFileName() method /** * Returns the file name component of the specified path. * @param path The path * @since jEdit 3.1pre4 */ public String getFileName(String path) { if(path.equals("/")) return path; while(path.endsWith("/") || path.endsWith(File.separator)) path = path.substring(0,path.length() - 1); int index = Math.max(path.lastIndexOf('/'), path.lastIndexOf(File.separatorChar)); if(index == -1) index = path.indexOf(':'); // don't want getFileName("roots:") to return "" if(index == -1 || index == path.length() - 1) return path; return path.substring(index + 1); } //}}} //{{{ getFilePath() method /** * Returns the path component of the specified VFS path. * The standard implementation cuts off the protocol * and the protocol separator character and then delegates * to eventually present sub-VFS-paths present in the VFS path * like "jode:archive:/test.zip!/test.txt". * <p> * If a VFS implementation can have additional * information in the VFS path like username / password / host / port * for FTP VFS or archive filename for archive VFS, this * method should be overridden to remove those information also. * The easiest would be to remove those additional information * and then delegate to {@code super.getFilePath()}. * * @param vfsPath The VFS path * @since jEdit 4.5pre1 */ public String getFilePath(String vfsPath) { if (!MiscUtilities.isURL(vfsPath)) return vfsPath; String filePath = vfsPath.substring(MiscUtilities.getProtocolOfURL(vfsPath).length() + 1); return VFSManager.getVFSForPath(filePath).getFilePath(filePath); } //}}} //{{{ getParentOfPath() method /** * Returns the parent of the specified path. This must be * overridden to return a non-null value for browsing of this * filesystem to work. * @param path The path * @since jEdit 2.6pre5 */ public String getParentOfPath(String path) { // ignore last character of path to properly handle // paths like /foo/bar/ int lastIndex = path.length() - 1; while(lastIndex > 0 && (path.charAt(lastIndex) == File.separatorChar || path.charAt(lastIndex) == '/')) { lastIndex--; } int count = Math.max(0,lastIndex); int index = path.lastIndexOf(File.separatorChar,count); if(index == -1) index = path.lastIndexOf('/',count); if(index == -1) { // this ensures that getFileParent("protocol:"), for // example, is "protocol:" and not "". index = path.lastIndexOf(':'); } return path.substring(0,index + 1); } //}}} //{{{ constructPath() method /** * Constructs a path from the specified directory and * file name component. This must be overridden to return a * non-null value, otherwise browsing this filesystem will * not work.<p> * * Unless you are writing a VFS, this method should not be called * directly. To ensure correct behavior, you <b>must</b> call * {@link org.gjt.sp.jedit.MiscUtilities#constructPath(String,String)} * instead. * * @param parent The parent directory * @param path The path * @since jEdit 2.6pre2 */ public String constructPath(String parent, String path) { return parent + path; } //}}} //{{{ getFileSeparator() method /** * Returns the file separator used by this VFS. * @since jEdit 2.6pre9 */ public char getFileSeparator() { return '/'; } //}}} //{{{ getTwoStageSaveName() method /** * Returns a temporary file name based on the given path. * * By default jEdit first saves a file to <code>#<i>name</i>#save#</code> * and then renames it to the original file. However some virtual file * systems might not support the <code>#</code> character in filenames, * so this method permits the VFS to override this behavior. * * If this method returns <code>null</code>, two stage save will not * be used for that particular file (introduced in jEdit 4.3pre1). * * @param path The path name * @since jEdit 4.1pre7 */ public String getTwoStageSaveName(String path) { return MiscUtilities.constructPath(getParentOfPath(path), '#' + getFileName(path) + "#save#"); } //}}} //{{{ reloadDirectory() method /** * Called before a directory is reloaded by the file system browser. * Can be used to flush a cache, etc. * @since jEdit 4.0pre3 */ public void reloadDirectory(String path) {} //}}} //{{{ createVFSSession() methods /** * Creates a VFS session. This method is called from the AWT thread, * so it should not do any I/O. It could, however, prompt for * a login name and password, for example. A simpler filesystem * may set the <code>NON_AWT_SESSION_CAP</code> capability. When set, * sessions may be obtained from any thread. * @param path The path in question * @param comp The component that will parent any dialog boxes shown * @return The session. The session can be null if there were errors * @since jEdit 2.6pre3 */ public Object createVFSSession(String path, Component comp) { return new Object(); } /** * Same as {@link #createVFSSession}, but may be called from any * thread. It first checks the <code>NON_AWT_SESSION_CAP</code> * capability and enters EDT thread if necessary. */ public Object createVFSSessionSafe(final String path, final Component comp) { Object session = null; if ((getCapabilities() & NON_AWT_SESSION_CAP) != 0) { session = createVFSSession(path, comp); } else { SessionGetter getter = new SessionGetter(path, comp); ThreadUtilities.runInDispatchThreadAndWait(getter); session = getter.get(); } return session; } //}}} //{{{ load() method /** * Loads the specified buffer. The default implementation posts * an I/O request to the I/O thread. * @param view The view * @param buffer The buffer * @param path The path */ public boolean load(View view, Buffer buffer, String path) { if((getCapabilities() & READ_CAP) == 0) { VFSManager.error(view,path,"vfs.not-supported.load",new String[] { name }); return false; } Object session = createVFSSession(path,view); if(session == null) return false; if((getCapabilities() & WRITE_CAP) == 0) buffer.setReadOnly(true); Task request = new BufferLoadRequest(view, buffer, session, this, path); if(buffer.isTemporary()) // this makes HyperSearch much faster request.run(); else // BufferLoadRequest can cause UI interations (for example FTP connection dialog), // so it should be runned in Dispatch thread //ThreadUtilities.runInDispatchThread(request); ThreadUtilities.runInBackground(request); return true; } //}}} //{{{ save() method /** * Saves the specifies buffer. The default implementation posts * an I/O request to the I/O thread. * @param view The view * @param buffer The buffer * @param path The path */ public boolean save(View view, Buffer buffer, String path) { if((getCapabilities() & WRITE_CAP) == 0) { VFSManager.error(view,path,"vfs.not-supported.save",new String[] { name }); return false; } Object session = createVFSSession(path,view); if(session == null) return false; /* When doing a 'save as', the path to save to (path) * will not be the same as the buffer's previous path * (buffer.getPath()). In that case, we want to create * a backup of the new path, even if the old path was * backed up as well (BACKED_UP property set) */ if(!path.equals(buffer.getPath())) buffer.unsetProperty(Buffer.BACKED_UP); ThreadUtilities.runInBackground(new BufferSaveRequest( view,buffer,session,this,path)); return true; } //}}} //{{{ copy() methods /** * Copy a file to another using VFS. * * @param progress the progress observer. It could be null if you don't want to monitor progress. If not null * you should probably launch this command in a WorkThread * @param sourceVFS the source VFS * @param sourceSession the VFS session * @param sourcePath the source path. It must be a file and must exists * @param targetVFS the target VFS * @param targetSession the target session * @param targetPath the target path. * If it is a path, it must exists, if it is a file, the parent must * exists * @param comp The component that will parent error dialog boxes * @param canStop could this copy be stopped ? * @return true if the copy was successful * @throws IOException IOException If an I/O error occurs * @since jEdit 4.3pre3 */ public static boolean copy(ProgressObserver progress, VFS sourceVFS, Object sourceSession,String sourcePath, VFS targetVFS, Object targetSession,String targetPath, Component comp, boolean canStop) throws IOException { return copy(progress, sourceVFS, sourceSession, sourcePath, targetVFS, targetSession, targetPath, comp, canStop, true); } /** * Copy a file to another using VFS. * * @param progress the progress observer. It could be null if you don't want to monitor progress. If not null * you should probably launch this command in a WorkThread * @param sourceVFS the source VFS * @param sourceSession the VFS session * @param sourcePath the source path. It must be a file and must exists * @param targetVFS the target VFS * @param targetSession the target session * @param targetPath the target path. * If it is a path, it must exists, if it is a file, the parent must * exists * @param comp The component that will parent error dialog boxes * @param canStop could this copy be stopped ? * @param sendVFSUpdate true if you want to send a VFS update after the copy. False otherwise (if you do a lot * of copy) * @return true if the copy was successful * @throws IOException IOException If an I/O error occurs * @since jEdit 5.0 */ public static boolean copy(ProgressObserver progress, VFS sourceVFS, Object sourceSession,String sourcePath, VFS targetVFS, Object targetSession,String targetPath, Component comp, boolean canStop, boolean sendVFSUpdate) throws IOException { if (sourcePath.equals(targetPath)) { Log.log(Log.WARNING, VFS.class, jEdit.getProperty("ioerror.copy-self", new Object[] { sourcePath })); return false; } if (progress != null) progress.setStatus("Initializing"); InputStream in = null; OutputStream out = null; try { VFSFile sourceVFSFile = sourceVFS._getFile(sourceSession, sourcePath, comp); if (sourceVFSFile == null) throw new FileNotFoundException("source path " + sourcePath + " doesn't exists"); if (progress != null) { progress.setMaximum(sourceVFSFile.getLength()); } VFSFile targetVFSFile = targetVFS._getFile(targetSession, targetPath, comp); if (targetVFSFile == null) { String parentTargetPath = MiscUtilities.getParentOfPath(targetPath); VFSFile parentTargetVFSFile = targetVFS._getFile(targetSession, parentTargetPath, comp); if (parentTargetVFSFile == null) throw new FileNotFoundException("target path " + parentTargetPath + " doesn't exists"); if (parentTargetVFSFile.getType() == VFSFile.DIRECTORY) { String targetFilename = MiscUtilities.getFileName(targetPath); targetPath = MiscUtilities.constructPath(parentTargetPath, targetFilename); } else { throw new IOException("The parent of target path is a file"); } } else if (targetVFSFile.getType() == VFSFile.DIRECTORY) { if (targetVFSFile.getPath().equals(sourceVFSFile.getPath())) return false; targetPath = MiscUtilities.constructPath(targetPath, sourceVFSFile.getName()); } in = new BufferedInputStream(sourceVFS._createInputStream(sourceSession, sourcePath, false, comp)); out = new BufferedOutputStream(targetVFS._createOutputStream(targetSession, targetPath, comp)); boolean copyResult = IOUtilities.copyStream(IOBUFSIZE, progress, in, out, canStop); if (sendVFSUpdate) VFSManager.sendVFSUpdate(targetVFS, targetPath, true); return copyResult; } finally { IOUtilities.closeQuietly((Closeable)in); IOUtilities.closeQuietly((Closeable)out); } } /** * Copy a file to another using VFS. * * @param progress the progress observer. It could be null if you don't want to monitor progress. If not null * you should probably launch this command in a WorkThread * @param sourcePath the source path * @param targetPath the target path * @param comp The component that will parent error dialog boxes * @param canStop if true the copy can be stopped * @param sendVFSUpdate true if you want to send a VFS update after the copy. False otherwise (if you do a lot * of copy) * @return true if the copy was successful * @throws IOException IOException If an I/O error occurs * @since jEdit 5.0 */ public static boolean copy(ProgressObserver progress, String sourcePath,String targetPath, Component comp, boolean canStop, boolean sendVFSUpdate) throws IOException { VFS sourceVFS = VFSManager.getVFSForPath(sourcePath); VFS targetVFS = VFSManager.getVFSForPath(targetPath); Object sourceSession = null; Object targetSession = null; try { sourceSession = sourceVFS.createVFSSession(sourcePath, comp); if (sourceSession == null) { Log.log(Log.WARNING, VFS.class, "Unable to get a valid session from " + sourceVFS + " for path " + sourcePath); return false; } targetSession = targetVFS.createVFSSession(targetPath, comp); if (targetSession == null) { Log.log(Log.WARNING, VFS.class, "Unable to get a valid session from " + targetVFS + " for path " + targetPath); return false; } return copy(progress, sourceVFS, sourceSession, sourcePath, targetVFS, targetSession, targetPath, comp,canStop, sendVFSUpdate); } finally { if (sourceSession != null) sourceVFS._endVFSSession(sourceSession, comp); if (targetSession != null) targetVFS._endVFSSession(targetSession, comp); } } /** * Copy a file to another using VFS. * * @param progress the progress observer. It could be null if you don't want to monitor progress. If not null * you should probably launch this command in a WorkThread * @param sourcePath the source path * @param targetPath the target path * @param comp The component that will parent error dialog boxes * @param canStop if true the copy can be stopped * @return true if the copy was successful * @throws IOException IOException If an I/O error occurs * @since jEdit 4.3pre3 */ public static boolean copy(ProgressObserver progress, String sourcePath,String targetPath, Component comp, boolean canStop) throws IOException { return copy(progress, sourcePath, targetPath, comp, canStop, true); } //}}} //{{{ insert() method /** * Inserts a file into the specified buffer. The default implementation * posts an I/O request to the I/O thread. * @param view The view * @param buffer The buffer * @param path The path */ public boolean insert(View view, Buffer buffer, String path) { if((getCapabilities() & READ_CAP) == 0) { VFSManager.error(view,path,"vfs.not-supported.load",new String[] { name }); return false; } Object session = createVFSSession(path,view); if(session == null) return false; ThreadUtilities.runInBackground(new BufferInsertRequest( view,buffer,session,this,path)); return true; } //}}} // A method name that starts with _ requires a session object //{{{ _canonPath() method /** * Returns the canonical form of the specified path name. For example, * <code>~</code> might be expanded to the user's home directory. * @param session The session * @param path The path * @param comp The component that will parent error dialog boxes * @exception IOException if an I/O error occurred * @since jEdit 4.0pre2 */ public String _canonPath(Object session, String path, Component comp) throws IOException { return path; } //}}} //{{{ _listDirectory() method /** * A convenience method that matches file names against globs, and can * optionally list the directory recursively. * @param session The session * @param directory The directory. Note that this must be a full * URL, including the host name, path name, and so on. The * username and password (if needed by the VFS) is obtained from the * session instance. * @param glob Only file names matching this glob will be returned * @param recursive If true, subdirectories will also be listed. * @param comp The component that will parent error dialog boxes * @exception IOException if an I/O error occurred * @since jEdit 4.1pre1 */ public String[] _listDirectory(Object session, String directory, String glob, boolean recursive, Component comp ) throws IOException { String[] retval = _listDirectory(session, directory, glob, recursive, comp, true, false); return retval; } //}}} //{{{ _listDirectory() method /** * A convenience method that matches file names against globs, and can * optionally list the directory recursively. * @param session The session * @param directory The directory. Note that this must be a full * URL, including the host name, path name, and so on. The * username and password (if needed by the VFS) is obtained from the * session instance. * @param glob Only file names matching this glob will be returned * @param recursive If true, subdirectories will also be listed. * @param comp The component that will parent error dialog boxes * @exception IOException if an I/O error occurred * @param skipBinary ignore binary files (do not return them). * This will slow down the process since it will open the files * @param skipHidden skips hidden files, directories, and * backup files. Ignores any file beginning with . or #, or ending with ~ * or .bak * * * @since jEdit 4.3pre5 */ public String[] _listDirectory(Object session, String directory, String glob, boolean recursive, Component comp, boolean skipBinary, boolean skipHidden) throws IOException { VFSFileFilter filter = new GlobVFSFileFilter(glob); return _listDirectory(session, directory, filter, recursive, comp, skipBinary, skipHidden); } //}}} //{{{ _listDirectory() method /** * A convenience method that filters the directory listing * according to a filter, and can optionally list the directory * recursively. * @param session The session * @param directory The directory. Note that this must be a full * URL, including the host name, path name, and so on. The * username and password (if needed by the VFS) is obtained from the * session instance. * @param filter The {@link VFSFileFilter} to use for filtering. * @param recursive If true, subdirectories will also be listed. * @param comp The component that will parent error dialog boxes * @exception IOException if an I/O error occurred * @param skipBinary ignore binary files (do not return them). * This will slow down the process since it will open the files * @param skipHidden skips hidden files, directories, and * backup files. Ignores any file beginning with . or #, or ending with ~ * or .bak * * @since jEdit 4.3pre7 */ public String[] _listDirectory(Object session, String directory, VFSFileFilter filter, boolean recursive, Component comp, boolean skipBinary, boolean skipHidden) throws IOException { List<String> files = new ArrayList<String>(100); listFiles(session,new HashSet<String>(), files,directory,filter, recursive, comp, skipBinary, skipHidden); String[] retVal = files.toArray(new String[files.size()]); Arrays.sort(retVal,new StandardUtilities.StringCompare<String>(true)); return retVal; } //}}} //{{{ _listFiles() method /** * Lists the specified directory. * @param session The session * @param directory The directory. Note that this must be a full * URL, including the host name, path name, and so on. The * username and password (if needed by the VFS) is obtained from the * session instance. * @param comp The component that will parent error dialog boxes * @exception IOException if an I/O error occurred * @since jEdit 4.3pre2 */ public VFSFile[] _listFiles(Object session, String directory, Component comp) throws IOException { VFSManager.error(comp,directory,"vfs.not-supported.list",new String[] { name }); return null; } //}}} //{{{ _getFile() method /** * Returns the specified directory entry. * @param session The session get it with {@link VFS#createVFSSession(String, Component)} * @param path The path * @param comp The component that will parent error dialog boxes * @exception IOException if an I/O error occurred * @return The specified directory entry, or null if it doesn't exist. * @since jEdit 4.3pre2 */ public VFSFile _getFile(Object session, String path, Component comp) throws IOException { return null; } //}}} //{{{ _delete() method /** * Deletes the specified URL. * @param session The VFS session * @param path The path * @param comp The component that will parent error dialog boxes * @exception IOException if an I/O error occurs * @since jEdit 2.7pre1 */ public boolean _delete(Object session, String path, Component comp) throws IOException { return false; } //}}} //{{{ _rename() method /** * Renames the specified URL. Some filesystems might support moving * URLs between directories, however others may not. Do not rely on * this behavior. * @param session The VFS session * @param from The old path * @param to The new path * @param comp The component that will parent error dialog boxes * @exception IOException if an I/O error occurs * @since jEdit 2.7pre1 */ public boolean _rename(Object session, String from, String to, Component comp) throws IOException { return false; } //}}} //{{{ _mkdir() method /** * Creates a new directory with the specified URL. * @param session The VFS session * @param directory The directory * @param comp The component that will parent error dialog boxes * @exception IOException if an I/O error occurs * @since jEdit 2.7pre1 */ public boolean _mkdir(Object session, String directory, Component comp) throws IOException { return false; } //}}} //{{{ _backup() method /** * Backs up the specified file. Default implementation in 5.0pre1 * copies the file to the backup directory. Before 5.0pre1 it was * empty. * @param session The VFS session * @param path The path * @param comp The component that will parent error dialog boxes * @exception IOException if an I/O error occurs * @since jEdit 3.2pre2 */ public void _backup(Object session, String path, Component comp) throws IOException { VFS vfsSrc = VFSManager.getVFSForPath(path); if (null == vfsSrc._getFile(session, path, comp)) // file to backup does not exist return; // maybe the file is not applicable to local filesystem // but don't worry - for backup purposes it may be out // of a real filesystem File backupDir = MiscUtilities.prepareBackupDirectory(path); if (backupDir == null) { Log.log(Log.WARNING, VFS.class, "Backup of remote file " + path + " failed, because there is no backup directory."); return; } if (!backupDir.exists()) { // Usually that means there is no specified backup // directory. Log.log(Log.WARNING, VFS.class, "Backup of file " + path + " failed. Directory " + backupDir + " does not exist."); return; } File backupFile = MiscUtilities.prepareBackupFile(path, backupDir); if (backupFile == null) { return; } // do copy using VFS.copy VFS vfsDst = VFSManager.getVFSForPath(backupFile.getPath()); Object sessionDst = vfsDst.createVFSSessionSafe( backupFile.getPath(), comp); if (sessionDst == null) { return; } try { if (!copy(null, vfsSrc, session, path, vfsDst, sessionDst, backupFile.getPath(), comp, true)) { Log.log(Log.WARNING, VFS.class, "Backup of file " + path + " failed. Copy to " + backupFile + " failed."); } } finally { vfsDst._endVFSSession(sessionDst, comp); } } //}}} //{{{ _createInputStream() method /** * Creates an input stream. This method is called from the I/O * thread. * @param session the VFS session * @param path The path * @param ignoreErrors If true, file not found errors should be * ignored * @param comp The component that will parent error dialog boxes * @return an inputstream or <code>null</code> if there was a problem * @exception IOException If an I/O error occurs * @since jEdit 2.7pre1 */ public InputStream _createInputStream(Object session, String path, boolean ignoreErrors, Component comp) throws IOException { VFSManager.error(comp,path,"vfs.not-supported.load",new String[] { name }); return null; } //}}} //{{{ _createOutputStream() method /** * Creates an output stream. This method is called from the I/O * thread. * @param session the VFS session * @param path The path * @param comp The component that will parent error dialog boxes * @exception IOException If an I/O error occurs * @since jEdit 2.7pre1 */ public OutputStream _createOutputStream(Object session, String path, Component comp) throws IOException { VFSManager.error(comp,path,"vfs.not-supported.save",new String[] { name }); return null; } //}}} //{{{ _saveComplete() method /** * Called after a file has been saved. * @param session The VFS session * @param buffer The buffer * @param path The path the buffer was saved to (can be different from * {@link org.gjt.sp.jedit.Buffer#getPath()} if the user invoked the * <b>Save a Copy As</b> command, for example). * @param comp The component that will parent error dialog boxes * @exception IOException If an I/O error occurs * @since jEdit 4.1pre9 */ public void _saveComplete(Object session, Buffer buffer, String path, Component comp) throws IOException {} //}}} //{{{ _finishTwoStageSave() method /** * Called after a file has been saved and we use twoStageSave (first saving to * another file). This should re-apply permissions for example. * @param session The VFS session * @param buffer The buffer * @param path The path the buffer was saved to (can be different from * {@link org.gjt.sp.jedit.Buffer#getPath()} if the user invoked the * <b>Save a Copy As</b> command, for example). * @param comp The component that will parent error dialog boxes * @exception IOException If an I/O error occurs * @since jEdit 4.3pre4 */ public void _finishTwoStageSave(Object session, Buffer buffer, String path, Component comp) throws IOException { } //}}} //{{{ _endVFSSession() method /** * Finishes the specified VFS session. This must be called * after all I/O with this VFS is complete, to avoid leaving * stale network connections and such. * @param session The VFS session * @param comp The component that will parent error dialog boxes * @exception IOException if an I/O error occurred * @since jEdit 2.7pre1 */ public void _endVFSSession(Object session, Component comp) throws IOException { } //}}} //{{{ getDefaultColorFor() method /** * Returns color of the specified file name, by matching it against * user-specified regular expressions. * @since jEdit 4.0pre1 */ public static Color getDefaultColorFor(String name) { synchronized(lock) { if(colors == null) loadColors(); for (ColorEntry entry : colors) { if (entry.re.matcher(name).matches()) return entry.color; } return null; } } //}}} //{{{ DirectoryEntryCompare class /** * Implementation of {@link Comparator} * interface that compares {@link VFSFile} instances. * @since jEdit 4.2pre1 */ public static class DirectoryEntryCompare implements Comparator<VFSFile> { private boolean sortIgnoreCase, sortMixFilesAndDirs; /** * Creates a new <code>DirectoryEntryCompare</code>. * @param sortMixFilesAndDirs If false, directories are * put at the top of the listing. * @param sortIgnoreCase If false, upper case comes before * lower case. */ public DirectoryEntryCompare(boolean sortMixFilesAndDirs, boolean sortIgnoreCase) { this.sortMixFilesAndDirs = sortMixFilesAndDirs; this.sortIgnoreCase = sortIgnoreCase; } public int compare(VFSFile file1, VFSFile file2) { if(!sortMixFilesAndDirs) { if(file1.getType() != file2.getType()) return file2.getType() - file1.getType(); } return StandardUtilities.compareStrings(file1.getName(), file2.getName(),sortIgnoreCase); } } //}}} //{{{ Private members private String name; private int caps; private String[] extAttrs; private static List<ColorEntry> colors; private static final Object lock = new Object(); //{{{ Class initializer static { EditBus.addToBus(new EBComponent() { public void handleMessage(EBMessage msg) { if(msg instanceof PropertiesChanged) { synchronized(lock) { colors = null; } } } }); } //}}} //{{{ recursive listFiles() method private void listFiles(Object session, Collection<String> stack, List<String> files, String directory, VFSFileFilter filter, boolean recursive, Component comp, boolean skipBinary, boolean skipHidden) throws IOException { if (recursive && !MiscUtilities.isURL(directory)) { String resolvedPath = MiscUtilities.resolveSymlinks(directory); /* * If looking at a symlink, do not traverse the * resolved path more than once. */ if (!directory.equals(resolvedPath)) { if (stack.contains(resolvedPath)) { Log.log(Log.ERROR,this, "Recursion in listFiles(): " + directory); return; } stack.add(resolvedPath); } } VFSFile[] _files = _listFiles(session,directory, comp); if(_files == null || _files.length == 0) return; for(int i = 0; i < _files.length; i++) { if (Thread.currentThread().isInterrupted()) break; VFSFile file = _files[i]; if (skipHidden && (file.isHidden() || MiscUtilities.isBackup(file.getName()))) continue; if(!filter.accept(file)) continue; if(file.getType() == VFSFile.DIRECTORY || file.getType() == VFSFile.FILESYSTEM) { if(recursive) { String canonPath = _canonPath(session, file.getPath(),comp); listFiles(session,stack,files, canonPath,filter,recursive, comp, skipBinary, skipHidden); } } else // It's a regular file { if (skipBinary) { try { if (file.isBinary(session)) { Log.log(Log.NOTICE,this ,file.getPath() + ": skipped as a binary file"); continue; } } catch(IOException e) { Log.log(Log.ERROR,this,e); // may be not binary... } } files.add(file.getPath()); } } } //}}} //{{{ loadColors() method private static void loadColors() { synchronized(lock) { colors = new ArrayList<ColorEntry>(); if(!jEdit.getBooleanProperty("vfs.browser.colorize")) return; String glob; int i = 0; while((glob = jEdit.getProperty("vfs.browser.colors." + i + ".glob")) != null) { try { colors.add(new ColorEntry( Pattern.compile(StandardUtilities.globToRE(glob)), jEdit.getColorProperty( "vfs.browser.colors." + i + ".color", Color.black))); } catch(PatternSyntaxException e) { Log.log(Log.ERROR,VFS.class,"Invalid regular expression: " + glob); Log.log(Log.ERROR,VFS.class,e); } i++; } } } //}}} //{{{ ColorEntry class private static class ColorEntry { Pattern re; Color color; ColorEntry(Pattern re, Color color) { this.re = re; this.color = color; } } //}}} //{{{ SessionGetter class private class SessionGetter implements Runnable { public SessionGetter(String path, Component comp) { this.path = path; this.comp = comp; } private Object session; private String path; private Component comp; public void run() { session = createVFSSession(path, comp); } public Object get() { return session; } } //}}} //}}} }