FileSystem
and
FileObject
.
From the perspective of the FileSystems API, all files consist of
byte streams (albeit with MIME types). Usually, the functionality of
this API will actually be used indirectly, via the
Loaders API,
which abstracts away most of the details of files and presents data in
terms of nodes.
At any given time, there may be several
Common Tasks
Basic operations using the FileSystems API should be mostly familiar,
and are similar to any file-access system. (There are a few, such as
monitoring files, which are not universally available on system-level
file systems, so you may want to take special note of these.) This
section gives examples of the more common tasks associated with using
file systems.
FileSystem
s
in the NetBeans
Repository
.
Some operations are specific to a particular file system, but often it
is possible to deal with files without having to keep track of which
file system they reside on--the mounted file systems then form a pool
of usable data files.
Mounting a new file system
It is not too likely that you would need to mount a file system from
within code (normally the user would mount new filesystems in the
Explorer), but if that is necessary it is not difficult:
Repository repo=TopManager.getDefault().getRepository();
LocalFileSystem fs=new LocalFileSystem();
fs.setRootDirectory(new File("c:\\some directory\\"));
repo.addFileSystem(fs);
If you are looking for a particular file system, you might try something like:
// The master file system:
FileSystem mainFS=repo.getDefaultFileSystem();
// A local file system by path name:
LocalFileSystem fsByPath=null;
Enumeration all=repo.getFileSystems();
while (all.hasMoreElements()) {
Object o=all.nextElement();
if (o instanceof LocalFileSystem) {
LocalFileSystem lfs=(LocalFileSystem) o;
if (lfs.getRootDirectory().equals(new File("c:\\some directory\\"))) {
fsByPath=lfs;
break;
}
}
}
Finding files and folders on existing file systems
Normally, you will be looking for a file or folder by name, and will want to get the
FileObject
which represents it. The easiest way to do this is straight from the repository:
FileObject file=repo.find("my.package", "MySource", "java");
// OR:
file=repo.findResource("my/package/MySource.java");
(That got the first file named
my/package/MySource.java
that could be found in any file
system. Occasionally you might want to find all files of a given name,
in different file systems; then you can use
Repository.findAll(...)
.)
If you need to get a file specifically from a particular file
system, just call find(...)
or
findResource(...)
on the file system object instead:
file=fs.find("my.package", "MySource", "java"); // OR: file=fs.findResource("my/package/MySource.java");(Note the similarity in usage to
ClassLoader.findResource(String)
.)
Or you may want to find the containing folder (represented also by
a
FileObject
):
FileObject folder=repo.find("my.package", null, null);To find all the folders and files directly contained in this folder, you may use:
FileObject children[]=folder.getChildren();Occasionally you may need to present a given file object as a URL (usable only within the IDE, of course); for example, to display it in a web browser. This is straightforward:
URL url=file.getURL (); TopManager.getDefault().showUrl(url);
FileObject subfolder=folder.createFolder("sub"); FileObject newfile=subfolder.createData("NewSource", "java");If you want to delete or rename a file, you must first take out a lock on the file, to make sure that no one else is actively using the file at the same time. Then you may perform the action on it:
FileLock lock = null; try { lock=newfile.lock(); } catch (FileAlreadyLockedException e) { // Try again later; perhaps display a warning dialog. return; } try { // Rename it. newfile.rename(lock, "NewSrc", "java"); // Delete it. newfile.delete(lock); } finally { // Always put this in a finally block! lock.releaseLock(); }If you want to move a file into a different directory (or even file system), you cannot use
rename(...)
; the easiest way is
to use a NetBeans helper method:
FileObject someFile=repo.find("my.pkg", "MySource", "java"); FileObject whereTo=repo.find("your.pkg", null, null); FileUtil.moveFile(someFile, whereTo, "YourSource");Note that in the current API set, it is neither possible nor necessary to lock folders (e.g. when creating new children), as normally locks are used to protect data files from conflicts between the Editor, the Explorer, and so on. If in the future there are thread-related problems associated with improper simultaneous access to the same folder, support for folder locking could be added to the FileSystems API.
Similarly, there is no support currently for nonexclusive read
locks--if you require exclusion of writers during a read, you must
take out a regular write lock for the duration of the read. This is
not normally necessary, since typically only the Editor will be
reading and writing the contents of the file, and other file
operations do not involve information which could be partially
corrupted between threads. If necessary, the API includes
facilities for
read-many/write-one locks.
Before reading all of the next two sections, be aware that for most
purposes it is sufficient to use a convenience implementation of
A particularly compelling reason to use The following methods ought to be implemented or overridden when
subclassing You need not actually override
Similarly,
Note that You must also implement
Also, many parts of the IDE store their own information in such
attributes--for example, the arguments for a file to be executed
are stored by
Similarly,
Besides what is mentioned here, the
First of all, you need not separately implement a file object--this
is taken care of for you. You need only provide some information about
how essential actions should be carried out.
You should still implement
You may cause the file system to automatically refresh itself at
periodic intervals, in case it is not possible to be notified of
changes directly. To do so, call
Most of the meat of the implementation is provided by setting
appropriate values for four protected variables, which should be
initialized in the constructor; each represents an implementation of a
separate interface handling one aspect of the file system.
Reading and writing files
Reading and writing the contents of a data file is straightforward:
BufferedReader from=new BufferedReader(new InputStreamReader(someFile.getInputStream()));
try {
String line;
while ((line=from.readLine()) != null) {
// do something with line
} finally {
from.close();
}
FileLock lock;
try {
lock=someFile.lock();
} catch (FileAlreadyLockedException e) {
return;
}
try {
PrintWriter to=new PrintWriter(someFile.getOutputStream(lock));
try {
to.println("testing...");
to.println("1..2..3..");
} finally {
to.close();
}
} finally {
lock.releaseLock();
}
Listening on file events
If you need to keep track of what is being done to a file by other
components, you can monitor it using normal Java events:
someFile.addFileChangeListener(new FileChangeAdapter() {
public void fileChanged(FileEvent ev) {
System.out.println("Contents changed.");
}
public void fileAttributeChanged(FileAttributeEvent ev) {
System.out.println(ev.getName() + ": " + ev.getOldValue() + " -> " + ev.getNewValue());
}
});
All events affecting existing files are actually fired twice, once
from the file itself and once from its containing folder, so you may
just want to listen on the parent folder. Also, file creation events
are fired on the folder only, of course:
FileObject someFolder=someFile.getParent();
someFolder.addFileChangeListener(new FileChangeAdapter() {
public void fileChanged(FileEvent ev) {
System.out.println("Contents of " + ev.getFile().getPackageNameExt('/', '.') + " changed.");
}
public void fileDataCreated(FileEvent ev) {
System.out.println("File " + ev.getFile().getPackageNameExt('/', '.') + " created.");
}
});
Implementing Custom File Systems
It is possible using the Filesystems API to implement a custom file
system from scratch which will appear in all ways to the IDE to be
interchangeable with the local or JAR file system types. This might be
useful for adding, e.g., support for browsing of URLs in the Explorer,
or integration with a proprietary file store used by an application
being integrated. There are no especially subtle issues involved in
doing this; but a number of methods must be implemented. This synopsis
will attempt to describe a fairly minimal set of things which should
be done to support a writable file system (read-only file systems are
of course easier); for other capabilities, the reader is referred to
the Javadoc.
FileSystem
which handles the rougher areas of caching,
synchronization, etc. that a file system implementation is likely to
encounter. FileSystem
permits somewhat finer control, but
typically AbstractFileSystem
should be used instead.
Subclassing
The basic technique to use for creating a new file system is to subclass
FileSystem
FileSystem
,
specializing a few methods. The subclass should have a default
constructor and have its relevant properties settable using JavaBeans,
so that the user can manipulate the file system in the Explorer, under
the Repository. For example, the
LocalFileSystem
exposes the properties rootDirectory
and
readOnly
as Bean properties, so these will show up on the
Property Sheet for the file system node. You may also want to create a
BeanInfo
so that an appropriate icon will be chosen, tooltips will work on the
properties, and so on.
BeanInfo
is
that many file systems will not be valid at all until configured
somehow by the user, typically to indicate "where" their root
folder should be. For such file systems, you should provide a
Customizer
component that can configure a new file system in a user-friendly
fashion (it should implement this interface and be a
Component
as well). To provide such a customizer, just
use the normal JavaBeans method of implementing
BeanInfo.getBeanDescriptor()
to create a BeanDescriptor
which
specifies
the customizer.
FileSystem
:
FileSystem.getDisplayName()
should get a short name for the filesystem that is presentable to the
user. You may want to use Java localization for this. For example, an
HTTP filesystem could use a URL as the name.
systemName
property should be set appropriately
for this file system. A good system name is an internal identifier
which uniquely identifies this file system; its main purpose is so
that references to files on the filesystem may be serialized with only
this name as an indication of
which
file system they are associated with (and not need to store a
Java-level object reference). So, it ought to remain constant across
restarts of the IDE--do not use
Object.hashCode()
,
for example! You should probably include both the class name for the
file system subclass, and some token indicating its root directory or
equivalent; for example,
com.mycom.myfs.HttpFileSystem/http://somewhere.com/file/system/root/
.
FileSystem.getSystemName()
and
FileSystem.setSystemName(String)
;
but you should call setSystemName(String)
from whatever Bean
setter method sets the location of the file system, so that it will
always be updated.
FileSystem.findResource(String)
is perhaps the most basic method to implement in this class--it
expects a qualified file name within the file system, and should
return the file object corresponding to that file (if it exists).
FileSystem.getRoot()
should just get the top-level root folder of the file system.
FileSystem.isReadOnly()
should indicate whether or not the file system is (currently or by
design) read-only. For example, the JAR file system automatically
returns true
; in the local file system, there is a
corresponding
setter method
allowing this to be a user-visible property which is dynamically
queried by destructive operations.
FileSystem.getActions()
should return an array of file-system-wide actions, i.e. operations
which could meaningfully be applied by the user to any file on this
file system. (If you have nothing you want in here, just return an
empty array.) For example, the local file system provides one global
action, Refresh, to resynchronize the file with that on disk; an HTTP
file system might provide a "Clear cache entry" option, etc.
FileSystem.prepareEnvironment(Environment)
should be overridden if the file system supports use in a VM-external
environment. Currently this means that it may be usefully added to a
system classpath for external execution, compilation, or debugging, so
it is only useful for file systems which somehow represent disk
directories or local JAR/ZIP archives, i.e. refinements of the
NetBeans-provided file systems. If you override it, just
add
an appropriate entry to the class path (e.g., the fully-qualified name
of a JAR file).
FileSystem.getStatus()
should be overridden for file systems that have some sort of
meaningful extra status that could be attached to files they own. The
most likely example is that a file system which implicitly handles
version control should have some way of indicating visually which
files have been checked out, are modified, and so on. By providing a
status recognizer, it may alter the names of resulting data nodes
(e.g. changing the displayed name of a data object from
MyForm
to MyForm [missing]
), or it may alter
the displayed icon in the Explorer (i.e. adding a question mark to the
corner of files whose status is unknown).
Subclassing
Implementing the actual files (and folders) is of course somewhat more
involved than the file system object. You should implement the
following methods at least:
FileObject
FileSystem.findResource(String)
and
FileSystem.getRoot()
.
FileObject.getFileObject(...)
,
FileObject.getChildren()
,
and
FileObject.getParent()
are the basic methods with which you implement the file system's hierarchy.
FileSystem.findResource(String)
could easily be
implemented in terms of FileSystem.getRoot()
and
FileObject.getFileObject(...)
.
FileObject.isData()
,
FileObject.isFolder()
,
FileObject.isRoot()
,
FileObject.getName()
,
and
FileObject.getExt()
permit further traversal of the file system hierarchy starting from a
file of unknown name and position.
FileObject.createData(...)
,
FileObject.createFolder(String)
,
FileObject.rename(...)
,
and
FileObject.delete(FileLock)
are the methods to override to support basic file-manipulation
operations on a writable file system.
FileObject.lock()
to create some sort of object which may be used to lock files for
rename(...)
and delete(...)
; this method
should likely be synchronized, and should keep track of whether there
is already a
FileLock
unreleased and associated with the file object.
FileObject.getInputStream()
and
FileObject.getOutputStream(FileLock)
are used to obtain read/write access to the actual contents of the
file. Note that getOutputStream
requires a lock, so you
do not need to worry about simultaneous write attempts.
FileObject.getSize()
must also be implemented for access to content to be useful, as well
as
FileObject.isReadOnly()
.
FileObject.lastModified()
is used during compilation (e.g.) to check whether certain
automatically-created files (such as Java classfiles) are out of date.
FileObject.addFileChangeListener(FileChangeListener)
and
FileObject.removeFileChangeListener(FileChangeListener)
may either be empty, in the case of a permanently read-only file system, or may use the normal
EventListenerList
support class, etc. You may also want to use
FileObject.fireFileChangedEvent(...)
and similar methods for convenience. In any case, on a writable file
system you should fire events for all the supported methods in
FileChangeListener
.
FileObject.getAttributes()
,
FileObject.getAttribute(String)
,
and
FileObject.setAttribute(...)
permit arbitrary name-value pairs to be associated with particular
files on a file system. For example, an HTTP file system might
store HTTP header lines as attributes if they were needed by some
other component.
ExecSupport
in the file's attributes. So for writable file systems, these
attributes should generally be supported; normally the abstract file system will do this for you.
FileObject.setImportant(boolean)
may have a dummy body but may also be used to control the backup
status of files, etc. For many filesystems this can be ignored;
typically version-control filesystems should use this to
automatically exclude "scratch" files, and similarly for the MS-DOS
"archive" attribute (if that were accessible from Java). For
example, a form (created by the Form Editor) will mark
*.form
and *.java
files as important, but
not *.class
files (since they can be regenerated
easily).
FileObject.isValid()
should, if possible, check whether the actual storage mechanism of the
file is still working--whether the file still exists on disk and has
not been erased without the IDE's knowledge, for example. It should be
fast, as it will be called quite frequently, so you should
avoid network access and so on.
FileObject.getMIMEType()
may need to be overridden for file systems that would have special
access to information about the meaning of a file's contents beyond
what is indicated by its extension (e.g. HTTP
Content-Type
headers).
Subclassing
This convenience implementation does much of the hard work of
AbstractFileSystem
FileSystem
and is generally more pleasant to create
subclasses of. Refer to the previous sections for details on what all
of these features do and how they interact with the rest of the
system.
AbstractFileSystem
subclass should still be
configurable as a JavaBean, just like the regular method.
AbstractFileSystem.getDisplayName()
to set the display name.
AbstractFileSystem.setRefreshTime(int)
,
e.g. from the constructor.
AbstractFileSystem.refreshRoot()
must be called if the file system supports changing of its root
directory (as the local file system does, for example).
List member
The variable
AbstractFileSystem.list
should contain an implementation of
AbstractFileSystem.List
.
This only specifies a way of accessing the list of children in a folder.
AbstractFileSystem.info
contains an
AbstractFileSystem.Info
which provides basic file statistics.
It also specifies the raw implementation of three basic types of actions: getting input and output streams for the file; locking and unlocking it physically (optional and not to be confused with locking within the IDE); and marking it as unimportant, i.e. not to be archived (again optional).
AbstractFileSystem.change
is an
AbstractFileSystem.Change
which provides the interface to create new folders and files; delete
them; and rename them (within the same directory).
AbstractFileSystem.attr
is an
AbstractFileSystem.Attr
allowing the IDE to read and write serializable attributes
(meta-information) to be associated with the file.
Here a word of caution is in order. The IDE frequently uses these
attributes for specific purposes, e.g. storing user settings for the
arguments used to execute a class. It is thus fairly important that
they be implemented correctly on a writable file system. Since many
concrete storage mechanisms (such as local disk) do not provide any
means of associating attributes in this way, the API provides a
default implementation which stores them in a special file (one per
folder), currently named .nbattrs (formerly
filesystem.attributes). The newer .nbattrs
files are XML-based; simple attribute values (primitive types,
String
s, and URL
s) are stored directly, and
more complex objects are serialized and the hexadecimal stream placed
in the XML. In the future it is expected that there will be a
mechanism for other attribute value types to be stored as proper XML.
In order to use this implementation, you should set the
attr
variable to be an instance of
DefaultAttributes
.
You need only provide the other three behaviors in its constructor for
it to function correctly--it manipulates the attribute files exactly
the same way as regular files would be manipulated.
However, it is undesirable for the attribute files to appear
externally as real files on the filesystem, since these would leave
them open to accidental modification, and they should of course not be
seen by the user. So, DefaultAttributes
also implements
AbstractFileSystem.List
as a filter, which
simply removes the attribute file before passing along the normal
children. To use this, you should have one private list object
containing your raw implementation of child scanning in the underlying
storage; then provide this to the DefaultAttributes
in
its constructor; and finally use the DefaultAttributes
object for AbstractFileSystem.list
as well as
AbstractFileSystem.attr
.
Subclassing an existing file system
If your file system provides very little functionality, but only a
minor hook or two, it may be more convenient to simply subclass
LocalFileSystem
or JarFileSystem
. This
should generally be straightforward, provided you remember always to
call the superclass method in an appropriate fashion when
overriding. For both of these filesystems you can override protected
methods corresponding to the methods in the AbstractFileSystem
methods (LocalFileSystem
uses DefaultAttributes
so
attribute-related methods are not provided). Of course, if you wind up
overriding more than a couple of these methods, you might as well just
subclass AbstractFileSystem
to begin with.
Adding the module descriptor
Please refer to the
Modules API
for details on how to create a module containing a custom file system.
Special File Systems
The FileSystems API includes a couple of special implementations which
are used by the core of the IDE to assemble the application, and could
also be used by module writers in some circumstances.
Using
This file system has no form of storage in and of itself. Rather, it
composes and proxies one or more "delegate" file systems. The result
is that of a "layered" sandwich of file systems, each able to provide
files to appear in the merged result. The layers are ordered so that a
file system in "front" can override one in "back".
MultiFileSystem
Creating a new MultiFileSystem
is easy in the simplest
cases: just call its
constructor
where you can pass it a list of delegates. If you pass it only
read-only delegates, the composite will also be read-only. Or you may
pass it one or more writable file systems (make sure the first file
system is one of them); then it will be able to act as a writable file
system, by default storing all actual changes on the first file
system.
This class is intended to be subclassed more than to be used as is, since typically a user of it will want finer-grain control over several aspects of its operation. When subclassed, delegates may be changed dynamically and the "contents" of the composite file system will automatically be updated.
One of the more common methods to override is
createWritableOn(String)
,
which should provide the delegate file system which should be used to
store changes to the indicated file (represented as a resource path).
Normally, this is hardcoded to provide the first file system (assuming
it is writable); subclasses may store changes on another file system,
or they may select a file system to store changes on according to (for
example) file extension. This could be used to separate source from
compiled/deployable files, for instance. Or some filesystems may
prefer to attempt to make changes in whatever file system provided the
original file, assuming it is writable; this is also possible, getting
information about the file's origin from
findSystem(FileObject)
.
Another likely candidate for overriding is
findResourceOn(...)
.
Normally this method just looks up resources by the normal means on
delegate file systems and provides the resultant file objects. However
it could be customized to "warp" the delegates somehow; for example,
selecting a different virtual root for one of them.
notifyMigration(FileObject)
provides a means for subclasses to update some state (for example,
visual annotations) when the file system used to produce a given file
changes dynamically. This most often happens when a file provided by a
back delegate is written to, and the result stored on the foremost
delegate, but it could occur in other situations too (e.g. change of
delegates, removal of overriding file on underlying front delegate,
etc.).
When new files are added to the MultiFileSystem
,
normally they will be physically added to the foremost delegate,
although as previously mentioned they can be customized. When their
contents (or attributes) are changed, by default these changes are
again made in the foremost delegate. But what if a file is deleted?
There must be a way to indicate on the foremost delegate that it
should not appear in the composite (even while it remains in one of
the delegates). For this reason, MultiFileSystem
uses
"masks" which are simply empty files named according to the file they
should hide, but with the suffix _hidden. Thus, for
example, if there is a file subdir/readme.txt_hidden in a
front delegate it will hide any files named
subdir/readme.txt in delegates further back. These masks
are automatically created as needed when files are "deleted" from
MultiFileSystem
; or delegate filesystems may explicitly
provide them. Normally the mask files are not themselves visible as
FileObject
s, since they are an artifact of the deletion
logic. However, when nesting MultiFileSystem
s inside
other MultiFileSystem
s, it can be useful to have
delegates be able to mask files not provided by their immediate
siblings, but by cousins. For this reason, nested subclasses may call
setProgagateMasks(true)
to make the mask files propagate up one or more nesting levels and
thus remain potent again cousin delegates.
Using
This file system is a simple implementation which is read-only and its
contents derived exclusively from an XML file. It is thus useful for
configuration and so on where a fixed set of (typically small) files
is needed. Its primary use is in modules which wish to provide a layer
for the default file system in the IDE (which stores configuration of
various sorts).
XMLFileSystem
Its Java API is very slim: common uses need only call the constructor taking a URL to the XML file as argument.
The XML format is also relatively simple. There is an
online DTD
for it, but descriptively: the document element is
<filesystem> and corresponds to the root folder;
subfolders are represented with the <folder>
element; and files within folders with the <file>
element. Files and folders must have names, with the name
attribute. Files need not specify contents, in which case they will be
zero-length; or they may specify contents loaded from some other
resource (url attribute, treated as relative to the base
URL of the document itself); or for small textual contents, the
contents may be included inline inside the element, typically using
the CDATA
syntax. Attributes may be specified on folders
or files as empty <attr> elements; the name must be
supplied, and the value in one of several formats: primitive types,
strings, URLs, arbitrary serialized objects in hexadecimal, or
computed on the fly by calling the default constructor of some class,
or a static method (the method may be passed the file object in
question and/or the attribute name, if you wish). So as an example to
install a template:
<?xml version="1.0"?> <!DOCTYPE filesystem PUBLIC "-//NetBeans//DTD Filesystem 1.0//EN" "http://www.netbeans.org/dtds/filesystem-1_0.dtd"> <filesystem> <folder name="Templates"> <folder name="Other"> <file name="foo.txt"> <!-- Using CDATA prevents random surrounding whitespace from appearing: --> <![CDATA[some contents here...]]> <!-- Make it a template: --> <attr name="template" boolvalue="true"/> <!-- Localized display name of data node: --> <attr name="SystemFileSystem.localizingBundle" stringvalue="com.foo.module.Bundle"/> <!-- HTML description of template for wizard to display: --> <attr name="templateWizardURL" urlvalue="nbresloc:/com/foo/module/foo-template-help.html"/> <!-- Set an iterator; complex object, so easiest to create thus: --> <attr name="templateWizardIterator" newvalue="com.foo.module.FooTemplateIterator"/> </file> </folder> </folder> </filesystem>where
FooTemplateIterator
contains a public constructor
taking no arguments.
FileObject
and Repository
diagrams represent
regular usage of the APi, while FileSystem
diagram is more
useful for developers that will want to implement their own filesystem.
FileObject
Repository
FileSystem