/*
* Sun Public License Notice
*
* The contents of this file are subject to the Sun Public License
* Version 1.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://www.sun.com/
*
* The Original Code is NetBeans. The Initial Developer of the Original
* Code is Sun Microsystems, Inc. Portions Copyright 1997-2003 Sun
* Microsystems, Inc. All Rights Reserved.
*/
package org.openide.filesystems;
import java.io.*;
import java.util.Enumeration;
import org.openide.util.enums.QueueEnumeration;
import org.openide.util.*;
/** This is the base for all implementations of file objects on a filesystem.
* Provides basic information about the object (its name, parent,
* whether it exists, etc.) and operations on it (move, delete, etc.).
*
* @author Jaroslav Tulach, Petr Hamernik, Ian Formanek
*/
public abstract class FileObject extends Object implements java.io.Serializable {
/** generated Serialized Version UID */
static final long serialVersionUID = 85305031923497718L;
/** Get the name without extension of this file or folder.
* Period at first position is not considered as extension-separator
* For the root folder of a filesystem, this will be the empty
* string (the extension will also be the empty string, and the
* fully qualified name with any delimiters will also be the
* empty string).
* @return name of the file or folder(in its enclosing folder)
*/
public abstract String getName ();
/** Get the extension of this file or folder.
* Period at first position is not considered as extension-separator
* This is the string after the last dot of the full name, if any.
*
* @return extension of the file or folder (if any) or empty string if there is none
*/
public abstract String getExt ();
/** Renames this file (or folder).
* Both the new basename and new extension should be specified.
* <p>
* Note that using this call, it is currently only possible to rename <em>within</em>
* a parent folder, and not to do moves <em>across</em> folders.
* Conversely, implementing filesystems need only implement "simple" renames.
* If you wish to move a file across folders, you should call {@link FileUtil#moveFile}.
* @param lock File must be locked before renaming.
* @param name new basename of file
* @param ext new extension of file (ignored for folders)
*/
public abstract void rename(FileLock lock, String name, String ext) throws IOException;
/** Copies this file. This allows the filesystem to perform any additional
* operation associated with the copy. But the default implementation is simple
* copy of the file and its attributes
*
* @param target target folder to move this file to
* @param name new basename of file
* @param ext new extension of file (ignored for folders)
* @return the newly created file object representing the moved file
*/
public FileObject copy (FileObject target, String name, String ext)
throws IOException {
if (isFolder()) {
throw new IOException(NbBundle.getBundle(FileObject.class).getString("EXC_FolderCopy"));
}
FileObject dest = FileUtil.copyFileImpl (this, target, name, ext);
return dest;
}
/** Moves this file. This allows the filesystem to perform any additional
* operation associated with the move. But the default implementation is encapsulated
* as copy and delete.
*
* @param lock File must be locked before renaming.
* @param target target folder to move this file to
* @param name new basename of file
* @param ext new extension of file (ignored for folders)
* @return the newly created file object representing the moved file
*/
public FileObject move (FileLock lock, FileObject target, String name, String ext)
throws IOException {
if (getParent ().equals (target)) {
// it is possible to do only rename
rename (lock, name, ext);
return this;
} else {
// have to do copy
FileObject dest = copy (target, name, ext);
delete(lock);
return dest;
}
}
/** Gets textual represtentation of this <code>FileObject</code>.
* <b>Note: Do not use it as full resource path. It is not safe.
* Subclasses may override it.
* For that purpose use {@link #getPath} directly.</b>
* <p>Typically it is useful for debugging purposes. Example of correct usage:
* <pre>
* <font class="type">FileObject</font> <font class="variable-name">fo</font> = getSomeFileObject();
* ErrorManager.getDefault().log(<font class="string">"Got a change from "</font> + fo);
* </pre>
* @return the full resource path of the file object
*/
public String toString() {
return getPath();
}
/** Get the full resource path of this file object starting from the filesystem root.
* Folders are separated with forward slashes. File extensions, if present,
* are included. The root folder's path is the empty string. The path of a folder
* never ends with a slash.
* <p>Subclasses are strongly encouraged to override this method.
* @return the path, for example <samp>path/from/root.ext</samp>
* @see FileSystem#findResource
* @since 3.7
*/
public String getPath() {
StringBuffer sb = new StringBuffer ();
constructName (sb, '/');
return sb.toString();
}
/** Get fully-qualified filename. Does so by walking through all folders
* to the root of the filesystem. Separates files with provided <code>separatorChar</code>.
* The extension, if present, is separated from the basename with <code>extSepChar</code>.
* <p><strong>Note:</strong> <samp>fo.getPath()</samp> will have the
* same effect as using this method with <samp>/</samp> and <samp>.</samp> (the standard
* path and extension delimiters).
* @param separatorChar char to separate folders and files
* @param extSepChar char to separate extension
* @return the fully-qualified filename
* @deprecated Please use the <a href="@JAVA/API@/org/netbeans/api/java/classpath/api.html">ClassPath API</a> instead.
*/
public String getPackageNameExt (char separatorChar, char extSepChar) {
StringBuffer sb = new StringBuffer ();
if (isRoot () || getParent ().isRoot ()) return getNameExt ();
getParent ().constructName (sb, separatorChar);
String ext = getExt ();
if (ext == null || ext.equals ("")) // NOI18N
sb.append (separatorChar).append (getNameExt ());
else
sb.append (separatorChar).append (getName ()).append (extSepChar).append (getExt ());
return sb.toString();
}
/** Get fully-qualified filename, but without extension.
* Like {@link #getPackageNameExt} but omits the extension.
* @param separatorChar char to separate folders and files
* @return the fully-qualified filename
* @deprecated Please use the <a href="@JAVA/API@/org/netbeans/api/java/classpath/api.html">ClassPath API</a> instead.
*/
public String getPackageName (char separatorChar) {
StringBuffer sb = new StringBuffer ();
if (isRoot () || getParent ().isRoot ())
return (isFolder ()) ? getNameExt() : getName ();
getParent ().constructName (sb, separatorChar);
sb.append (separatorChar).append ((isFolder ()) ? getNameExt() : getName ());
return sb.toString ();
}
/** Getter for name and extension of a file object. Dot is used
* as separator between name and ext.
* @return string name of the file in the folder (with extension)
*/
public String getNameExt () {
String n = getName ();
String e = getExt ();
return e == null || e.length () == 0 ? n : n + '.' + e;
}
/** Constructs path of file.
* @param sb string buffer
* @param sepChar separator character
*/
private void constructName (StringBuffer sb, char sepChar) {
FileObject parent = getParent ();
if ((parent != null) && !parent.isRoot ()) {
parent.constructName (sb, sepChar);
sb.append (sepChar);
}
sb.append (getNameExt ());
}
/** Get the filesystem containing this file.
* <p>
* Note that it may be possible for a stale file object to exist which refers to a now-defunct filesystem.
* If this is the case, this method will throw an exception.
* @return the filesystem
* @exception FileStateInvalidException if the reference to the file
* system has been lost (e.g., if the filesystem was deleted)
*/
public abstract FileSystem getFileSystem () throws FileStateInvalidException;
/** Get parent folder.
* The returned object will satisfy {@link #isFolder}.
*
* @return the parent folder or <code>null</code> if this object {@link #isRoot}.
*/
public abstract FileObject getParent ();
/** Test whether this object is a folder.
* @return true if the file object is a folder (i.e., can have children)
*/
public abstract boolean isFolder ();
/**
* Get last modification time.
* @return the date
*/
public abstract java.util.Date lastModified();
/** Test whether this object is the root folder.
* The root should always be a folder.
* @return true if the object is the root of a filesystem
*/
public abstract boolean isRoot ();
/** Test whether this object is a data object.
* This is exclusive with {@link #isFolder}.
* @return true if the file object represents data (i.e., can be read and written)
*/
public abstract boolean isData ();
/* Test whether the file is valid. The file can be invalid if it has been deserialized
* and the file no longer exists on disk; or if the file has been deleted.
*
* @return true if the file object is valid
*/
public abstract boolean isValid ();
/** Test whether there is a file with the same basename and only a changed extension in the same folder.
* The default implementation asks this file's parent using {@link #getFileObject(String name, String ext)}.
*
* @param ext the alternate extension
* @return true if there is such a file
*/
public boolean existsExt (String ext) {
FileObject parent = getParent ();
return parent != null && parent.getFileObject (getName (), ext) != null;
}
/** Delete this file. If the file is a folder and it is not empty then
* all of its contents are also recursively deleted.
*
* @param lock the lock obtained by a call to {@link #lock}
* @exception IOException if the file could not be deleted
*/
public abstract void delete (FileLock lock) throws IOException;
/** Delete this file. If the file is a folder and it is not empty then
* all of its contents are also recursively deleted. FileObject is locked
* before delete and finally is this lock released.
*
* @exception IOException if the file could not be deleted or
* FileAlreadyLockedException if the file is already locked {@link #lock}
* @since 1.15
*/
public final void delete () throws IOException {
FileLock lock = lock();
try {
delete (lock);
} finally {
lock.releaseLock();
}
}
/** Get the file attribute with the specified name.
* @param attrName name of the attribute
* @return appropriate (serializable) value or <CODE>null</CODE> if the attribute is unset (or could not be properly restored for some reason)
*/
abstract public Object getAttribute(String attrName);
/** Set the file attribute with the specified name.
* @param attrName name of the attribute
* @param value new value or <code>null</code> to clear the attribute. Must be serializable, although particular filesystems may or may not use serialization to store attribute values.
* @exception IOException if the attribute cannot be set. If serialization is used to store it, this may in fact be a subclass such as {@link NotSerializableException}.
*/
abstract public void setAttribute(String attrName, Object value) throws IOException;
/** Get all file attribute names for this file.
* @return enumeration of keys (as strings)
*/
abstract public Enumeration getAttributes();
/** Test whether this file has the specified extension.
* @param ext the extension the file should have
* @return true if the text after the last period (<code>.</code>) is equal to the given extension
*/
public final boolean hasExt (String ext) {
if (isHasExtOverride()) {
return hasExtOverride(ext);
}
return getExt ().equals (ext);
}
/** Overriden in AbstractFolder */
boolean isHasExtOverride() {
return false;
}
/** Overriden in AbstractFolder */
boolean hasExtOverride(String ext) {
return false;
}
/** Add new listener to this object.
* @param fcl the listener
*/
public abstract void addFileChangeListener (FileChangeListener fcl);
/** Remove listener from this object.
* @param fcl the listener
*/
public abstract void removeFileChangeListener (FileChangeListener fcl);
/** Fire data creation event.
* @param en enumeration of {@link FileChangeListener}s that should receive the event
* @param fe the event to fire in this object
*/
protected void fireFileDataCreatedEvent (Enumeration en, FileEvent fe ) {
dispatchEvent (FCLSupport.DATA_CREATED, en, fe);
}
/** Fire folder creation event.
* @param en enumeration of {@link FileChangeListener}s that should receive the event
* @param fe the event to fire in this object
*/
protected void fireFileFolderCreatedEvent (Enumeration en, FileEvent fe ) {
dispatchEvent (FCLSupport.FOLDER_CREATED, en, fe);
}
/** Fire file change event.
* @param en enumeration of {@link FileChangeListener}s that should receive the event
* @param fe the event to fire in this object
*/
protected void fireFileChangedEvent (Enumeration en, FileEvent fe ) {
dispatchEvent (FCLSupport.FILE_CHANGED, en, fe);
}
/** Fire file deletion event.
* @param en enumeration of {@link FileChangeListener}s that should receive the event
* @param fe the event to fire in this object
*/
protected void fireFileDeletedEvent (Enumeration en, FileEvent fe ) {
dispatchEvent (FCLSupport.FILE_DELETED, en, fe);
}
/** Fire file attribute change event.
* @param en enumeration of {@link FileChangeListener}s that should receive the event
* @param fe the event to fire in this object
*/
protected void fireFileAttributeChangedEvent (Enumeration en, FileAttributeEvent fe ) {
dispatchEvent (FCLSupport.ATTR_CHANGED, en, fe);
}
/** Fire file rename event.
* @param en enumeration of {@link FileChangeListener}s that should receive the event
* @param fe the event to fire in this object
*/
protected void fireFileRenamedEvent (Enumeration en, FileRenameEvent fe) {
dispatchEvent (FCLSupport.FILE_RENAMED, en, fe);
}
/** Puts the dispatch event into the filesystem.
*/
private final void dispatchEvent (int op, Enumeration en,
FileEvent fe
) {
try {
FileSystem fs = getFileSystem ();
fs.dispatchEvent (new ED(op, en, fe));
} catch (FileStateInvalidException ex) {
// no filesystem, no notification
}
}
/** Get the MIME type of this file.
* The MIME type identifies the type of the file's contents and should be used in the same way as in the <B>Java
* Activation Framework</B> or in the {@link java.awt.datatransfer} package.
* <P>
* The default implementation calls {@link FileUtil#getMIMEType}.
* (As a fallback return value, <code>content/unknown</code> is used.)
* @return the MIME type textual representation, e.g. <code>"text/plain"</code>; never <code>null</code>
*/
public String getMIMEType () {
String type = FileUtil.getMIMEType (this);
return type == null ? "content/unknown" : type; // NOI18N
}
/** Get the size of the file.
* @return the size of the file in bytes or zero if the file does not contain data (does not
* exist or is a folder).
*/
public abstract long getSize ();
/** Get input stream.
* @return an input stream to read the contents of this file
* @exception FileNotFoundException if the file does not exists, is a folder
* rather than a regular file or is invalid
*/
public abstract InputStream getInputStream () throws java.io.FileNotFoundException;
/** Get output stream.
* @param lock the lock that belongs to this file (obtained by a call to
* {@link #lock})
* @return output stream to overwrite the contents of this file
* @exception IOException if an error occures (the file is invalid, etc.)
*/
public abstract OutputStream getOutputStream (FileLock lock)
throws java.io.IOException;
/** Lock this file.
* @return lock that can be used to perform various modifications on the file
* @throws FileAlreadyLockedException if the file is already locked
* @throws UserQuestionException in case when the lock cannot be obtained now,
* but the underlaying implementation is able to do it after some
* complex/dangerous/long-lasting operation and request confirmation
* from the user
*
*/
public abstract FileLock lock () throws IOException;
// [???] Implicit file state is important.
/** Indicate whether this file is important from a user perspective.
* This method allows a filesystem to distingush between important and
* unimportant files when this distinction is possible.
* <P>
* <em>For example:</em> Java sources have important <code>.java</code> files and
* unimportant <code>.class</code> files. If the filesystem provides
* an "archive" feature it should archive only <code>.java</code> files.
* @param b true if the file should be considered important
*/
public abstract void setImportant (boolean b);
/** Get all children of this folder (files and subfolders). If the file does not have children
* (does not exist or is not a folder) then an empty array should be returned. No particular order is assumed.
*
* @return array of direct children
* @see #getChildren(boolean)
* @see #getFolders
* @see #getData
*/
public abstract FileObject[] getChildren ();
/** Enumerate all children of this folder. If the children should be enumerated
* recursively, first all direct children are listed; then children of direct subfolders; and so on.
*
* @param rec whether to enumerate recursively
* @return enumeration of type <code>FileObject</code>
*/
public Enumeration getChildren (final boolean rec) {
QueueEnumeration en = new QueueEnumeration () {
/** @param o processes object by adding its children to the queue */
public void process (Object o) {
FileObject fo = (FileObject)o;
if (rec && fo.isFolder ()) {
put (fo.getChildren ());
}
}
};
en.put (getChildren ());
return en;
}
/** Enumerate the subfolders of this folder.
* @param rec whether to recursively list subfolders
* @return enumeration of type <code>FileObject</code> (satisfying {@link #isFolder})
*/
public Enumeration getFolders (boolean rec) {
return new org.openide.util.enums.FilterEnumeration (getChildren (rec)) {
/** @return true if the object is of type FileFolder */
protected boolean accept (Object o) {
return ((FileObject)o).isFolder ();
}
};
}
/** Enumerate all data files in this folder.
* @param rec whether to recursively search subfolders
* @return enumeration of type <code>FileObject</code> (satisfying {@link #isData})
*/
public Enumeration getData (boolean rec) {
return new org.openide.util.enums.FilterEnumeration (getChildren (rec)) {
/** @return true if the object is of type FileFolder */
protected boolean accept (Object o) {
return ((FileObject)o).isData ();
}
};
}
/** Retrieve file or folder contained in this folder by name.
* <em>Note</em> that neither file nor folder is created on disk.
* @param name basename of the file or folder (in this folder)
* @param ext extension of the file; <CODE>null</CODE> or <code>""</code>
* if the file should have no extension or if folder is requested
* @return the object representing this file or <CODE>null</CODE> if the file
* or folder does not exist
* @exception IllegalArgumentException if <code>this</code> is not a folder
*/
public abstract FileObject getFileObject (String name, String ext);
/** Retrieve file or folder contained in this folder by name (no extension).
* <em>Note</em> that neither file nor folder is created on disk.
* @param name basename of the file (in this folder)
* @return the object representing this file or <CODE>null</CODE> if the file
* or folder does not exist
* @exception IllegalArgumentException if <code>this</code> is not a folder
*/
public final FileObject getFileObject (String name) {
return getFileObject (name, null);
}
/** Create a new folder below this one with the specified name. Fires
* <code>fileCreated</code> event.
*
* @param name the name of folder to create. Periods in name are allowed.
* @return the new folder
* @exception IOException if the folder cannot be created (e.g. already exists)
*/
public abstract FileObject createFolder (String name) throws IOException;
/** Create new data file in this folder with the specified name. Fires
* <code>fileCreated</code> event.
*
* @param name the name of data object to create (can contain a period)
* @param ext the extension of the file (or <code>null</code> or <code>""</code>)
*
* @return the new data file object
* @exception IOException if the file cannot be created (e.g. already exists)
*/
public abstract FileObject createData (String name, String ext) throws IOException;
/** Create new data file in this folder with the specified name. Fires
* <code>fileCreated</code> event.
*
* @param name the name of data object to create (can contain a period)
*
* @return the new data file object
* @exception IOException if the file cannot be created (e.g. already exists)
* @since 1.17
*/
public FileObject createData (String name) throws IOException {
return createData (name, "");// NOI18N
}
/** Test whether this file can be written to or not.
* <P>
* The value returned from this method should indicate the capabilities of the
* file from the point of view of users of the FileObject's API, the actual
* state of the file on a disk does not matter if the implementation of the
* filesystem can change it when requested.
* <P>
* The result returned from this method should be tight together with
* the expected behaviour of <code>getOutputStream</code>. If it is
* likely that the method successfully returns a stream that can be
* written to, let the <code>isReadOnly</code> return <code>false</code>.
* <P>
* Also other fileobject methods like <code>delete</code>
* are suggested to be connected to result of this method. If not
* read only, then it can be deleted, etc.
* <p>
* It is a good idea to call this method before attempting to perform any
* operation on the FileObject that might throw an IOException simply
* because it is read-only. If isReadOnly returns true, the operation may
* be skipped, or the user notified that it cannot be done.
* <em>However</em> it is often desirable for the user to be able to
* continue the operation in case the filesystem supports making a file
* writable. In this case calling code should:
* <ol>
* <li>Call {@link #lock} and catch any exception thrown.
* <li>Then:
* <ul>
* <li>If no exception is thrown, proceed with the operation.
* <li>If a <code>UserQuestionException</code> is thrown,
* call {@link org.openide.util.UserQuestionException#confirmed} on it
* (asynchronously - do not block any important threads). If <code>true</code>,
* proceed with the operation. If <code>false</code>, exit.
* If an <code>IOException</code> is thrown, notify it and exit.
* <li>If another <code>IOException</code> is thrown, call {@link #isReadOnly}.
* If <code>true</code>, ignore the exception (it is expected).
* If <code>false</code>, notify it.
* </ul>
* In either case, exit.
* </ol>
* <p>
*
* @return <CODE>true</CODE> if file is read-only
* @deprecated Please use the {@link #canWrite}.
*/
public abstract boolean isReadOnly ();
/**
* Tests if this file can be written to.
* <P>
* The default implementation simply uses <code> java.io.File.canWrite </code>
* if there exists conversion to <code> java.io.File</code> (see {@link FileUtil#toFile}).
* If conversion is not possible, then deprecated method {@link #isReadOnly} is used.
* @return true if this file can be written, false if not.
* @since 3.31
*/
public boolean canWrite () {
File f = FileUtil.toFile(this);
if (f != null)
return f.canWrite();
return !isReadOnly ();
}
/**
* Tests if this file can be read.
* <P>
* The default implementation simply uses <code> java.io.File.canRead </code>
* if there exists conversion to <code> java.io.File</code> (see {@link FileUtil#toFile}).
* If conversion is not possible, then <code>true </code> is returned.
* @return true if this file can be read, false if not.
* @since 3.31
*/
public boolean canRead () {
File f = FileUtil.toFile(this);
if (f != null)
return f.canRead();
return true;
}
/** Should check for external modifications. For folders it should reread
* the content of disk, for data file it should check for the last
* time the file has been modified.
*
* @param expected should the file events be marked as expected change or not?
* @see FileEvent#isExpected
*/
public void refresh (boolean expected) {
}
/** Should check for external modifications. For folders it should reread
* the content of disk, for data file it should check for the last
* time the file has been modified.
* <P>
* The file events are marked as unexpected.
*/
public void refresh () {
refresh (false);
}
/** Get URL that can be used to access this file.
* The URL is only usable within the IDE as it uses a special protocol handler.
* @return URL of this file object
* @exception FileStateInvalidException if the file is not valid
*/
public final java.net.URL getURL() throws FileStateInvalidException {
return URLMapper.findURL(this, URLMapper.INTERNAL);
}
/**
* Tests if file really exists or is missing. Some operation on it may be restricted.
* @return true indicates that the file is missing.
* @since 1.9
*/
public boolean isVirtual() {
return false;
}
private class ED extends FileSystem.EventDispatcher {
private int op;
private Enumeration en;
private FileEvent fe;
public ED( int op, Enumeration en, FileEvent fe ) {
this.op = op;
this.en = en;
this.fe = fe;
}
/** Listeners registered from MultiFileObject are considered as priority
* listeners.
*/
private boolean isPriorityListener (FileChangeListener fcl) {
return (fcl instanceof MultiFileObject.MfoWeakListener);
}
/** @param onlyPriority if true then invokes only priority listeners
* else all listeners are invoked.
*/
protected void dispatch(boolean onlyPriority) {
QueueEnumeration newEnum = new QueueEnumeration();// later lazy
while (en.hasMoreElements ()) {
FileChangeListener fcl = (FileChangeListener)en.nextElement ();
if (onlyPriority && !isPriorityListener(fcl)) {
newEnum.put(fcl);
continue;
}
FileObject fo = fe.getFile();
if (fo != null && (op == FCLSupport.DATA_CREATED || op == FCLSupport.FOLDER_CREATED)) {
op = (fo.isFolder())?FCLSupport.FOLDER_CREATED:FCLSupport.DATA_CREATED;
}
FCLSupport.dispatchEvent(fcl, fe, op);
}
if (onlyPriority)
this.en = newEnum;
/** FileEvents are forked in may cases. But FileEvents fired from
* FileSystem and from Repository mustn`t be forked.
*/
FileObject fo = fe.getFile();
boolean transmit = (fo != null) && !fo.equals(fe.getSource());
if (!en.hasMoreElements() && transmit && !onlyPriority) {
FileSystem fs = null;
Repository rep = null;
try {
fs = fe.getFile().getFileSystem();
rep = fs.getRepository ();
} catch (FileStateInvalidException fsix) {return;}
fs.getFCLSupport ().dispatchEvent (fe, op);
if (rep != null)
rep.getFCLSupport ().dispatchEvent (fe, op);
}
}
protected void setAtomicActionLink (EventControl.AtomicActionLink propID) {
fe.setAtomicActionLink (propID);
}
}
}