Introduction to the NetBeans Idioms and Infrastructure
Feedback
This overview will quickly familiarize you with how NetBeans plug-in modules
interact with the NetBeans infrastructure and with each other. It is not intended
as a comprehensive document -- the NetBeans API List goes into greater detail --
but should serve as a guide to understanding the basic concepts of NetBeans plug-in module
development.
A key thing to understanding NetBeans is to realize that very often in
NetBeans, the same API or infrastructure does double-duty - playing one role
in dealing with the user's files on disk, and another role when it comes to
configuration information and runtime data. For example:
- A FileSystem represents the
user's files, but the System Filesystem represents the IDE's configuration
data.
- A DataObject represents the parsed content of a Java or other file, but
DataObjects also are used to instantiate a Java object installed
by a module.
Lookup.getDefault() is the way you access global
services and singletons, but you also call Node.getLookup() to
find services specific to an individual file or object.
It is this reuse that has led some to say the NetBeans APIs are confusing, and it
is the purpose of this overview to rapidly familiarize you with what these things
are and how they are used in both roles.
In NetBeans 3.x, adding items to the classpath was accomplished by "mounting"
FileSystems - a FileSystem had a root directory and everything under it
amounted to a virtual namespace in which files lived.
Since NetBeans 4.0, the "mounting" is gone, and FileSystems are not
a concept that users are exposed to in the UI - but the infrastructure
behind FileSystems - org.openide.filesystems.FileSystem is alive
and well under the hood. In coding NetBeans modules, you will typically
interact with instances of org.openide.filesystems.FileObject,
not java.io.File.
Differences Between java.io.File and FileObjects
The main differences between them are as follows:
- You get FileObjects from a FileSystem, rather than create them with a constructor.
- Typically you don't have FileObjects which represent something that doesn't
exist (as you can with
new File ("some/place/that/doesnt/exist")).
- You can listen for changes on FileObjects, including listening on folders
for changes that happen anywhere underneath them
- FileObjects don't necessarily represent actual files on disk
- FileObjects can have attributes which are essentially key-value
pairs that can be associated with a file. An attribute might be a
string, or a serialized object (note that use of attributes on
user files on disk is discouraged as of NetBeans 4.0, but they are
still commonly used in configuration files).
- The path separator for FileObjects is always
/, no
conversions with File.separator are needed
FileSystems are used in two basic but very distinct ways in NetBeans. The
first is representing the user's files on disk. To get a FileObject for some
path in NetBeans, just call, e.g.
Repository.getDefault().getDefaultFilesystem().getRoot().getFileObject("path/to/some/File.txt");
The
Repository is the master registry of all filesystems
NetBeans knows about. In 4.0, it is the contents of the user's hard drive(s).
The second usage is to represent configuration data - this is the "System Filesystem",
which is where modules can install their files. Folders in the
System Filesystem act as "extension points" - there are some which
have predefined meanings (for example, NetBeans' main menu is a tree of
folders you will place special "files" into to add menu items);
modules are free to create their own folders and do as they wish with the
contents.
How does all this work? Well, once you have the concept of a virtualized
FileSystem full of FileObjects,
it's relatively easy to imagine a FileSystem
which took several other FileSystems as arguments, and presented a merged
view of the sub-filesystems as if all the data lived in one tree.
Add into this the notion that the "files" in a FileSystem
don't actually have to be physical files on disk at all - anything that
can be made to walk and talk like a file will do. So you could have an
XML "filesystem" where the contents of files lived in an
XML document, not a bunch of files on disk.
XML Layers
That is what NetBeans does: Each module can define an XML "layer"
file, which contains some virtual "files" and folders that are merged into
the System Filesystem. In this way modules add their configuration data
to the system. And because the System Filesystem is composed from discrete
XML fragments from modules, when a module is disabled or unloaded, its
XML layer is simply removed.
FileObjects for the various folders that
had files removed from them fire changes indicating some files were
deleted, so the UI can get rid of any objects that represented the now-unloaded
module's files. This is why you can uninstall and reload modules at runtime.
In its jar manifest, a module will contain a line such as:
OpenIDE-Module-Layer: org/netbeans/modules/mymodule/layer.xml
This is a pointer to an XML file inside the module jar (meaning that you
simply create this file somewhere in your sources so it will be compiled
into the jar when your module is built). In its simplest form, that could contain
something like:
<filesystem>
<folder name="myFolder">
<file name="myFile.txt" url="resources/aTextFile.txt"/>
</folder>
</filesystem>
The
url attribute is important: It says where the contents
of
myFile.txt lives in the module's jar file. This path is
relative to the location of the layer file. So, if the layer file is
org/netbeans/modules/mymodule/layer.xml, then in the module
jar there should also be a text file
org/netbeans/modules/mymodule/resources/aTextFile.txt. When
some code requests an
InputStream for
myFolder/myFile.txt,
that text file in the module jar is what will actually be read.
Of course, this particular fragment doesn't do much of anything, but it is
useful to illustrate what can be done here. Since myFolder
has no predefined purpose to NetBeans, it is up to the module defining
that folder to do something with its contents. But one could imagine
a module that provided myFolder, let other modules add more
files to that folder, and provided one menu item for each file, letting
the user view them.
Accessing this file programmatically is quite simple:
FileObject myFile = Repository.getDefault().getDefaultFileSystem().findResource ("myFolder/myFile.txt");
InputStream in = myFile.getInputStream();
//...do something with it
Providing Java Objects through Module Layers
Just being able to install text files isn't terribly interesting. Where the
system of layers gets its power is in the ability to make files act as
factories for Java objects. This is made possible using the same
infrastructure that recognizes user data on disk, which will be discussed
in more detail in
the section on Loaders. Effectively,
there is a specific file-extension registered in the system,
.instance
which identifies a file that actually represents a Java object and can create
the actual object.
<filesystem>
<folder name="Menu">
<folder name="File">
<file name="org-netbeans-modules-mymodule-MyAction.instance"/>
</folder>
</folder>
</filesystem>
The above module layer actually adds a Swing Action (implemented by the class
org.netbeans.modules.mymodule.MyAction) into the File menu on the
main menu bar in NetBeans. The NetBeans core defines the folder
Menu;
the
core/ui defines common menus that are in NetBeans, and provides
the infrastructure that listens on these folders and keeps the GUI up-to-date if
things are added or removed. Toolbars work in a similar fashion, as do many other
things in NetBeans.
Hiding Files in the System Filesystem
The System Filesystem also allows one module to remove what another module
adds. The semantics are extremely simple - for example, if you wanted to delete
the File menu in NetBeans when your module is enabled, simply put the following
into your module layer:
<filesystem>
<folder name="Menu">
<folder name="File_hidden"/>
</folder>
</filesystem>
To make this work, modules can, in their manifest, request to be installed only
after another module is installed - thus there is a defined stacking order to
module layers.
The System Filesystem is Read-Write
If it were all just static XML fragments, it wouldn't be possible to actually
store configuration changes the user has made - but of course, this is possible.
Recall that we have the notion of a filesystem composed of merging multiple
other filesystems - and that we know that we have an implementation of
FileSystem over actual files on disk, which is how a user's data
files are accessed.
The top layer to the system filesystem is the config/ subdirectory
of the user's settings directory - typically this lives in the user's home
directory under the directory .netbeans. So when a user makes
changes (like rearranging menu items), the diff of the changes is written
to disk in the settings directory; since this layer lives at the top of the
stack, whatever changes are there (such as hiding files, as discussed above),
override anything a module has in its layer file.
DataObjects are wrappers for
FileObjects. A FileObject simply represents a file-like
entity;
DataObjects are the level at which the system understands what the contents
of a file are. So a module that implements handling for a particular file
type provides its own subclass of
DataObject and a factory which can create an instance of that DataObject type
when it is passed a
FileObject.
DataObjects are what provide programmatic
access to the contents of a file - such as parsing a file and providing a
model for its content.
The factory for these objects, which a module installs, is called a
DataLoader. It is declared directly in the module's manifest:
Name: org/netbeans/modules/povray/PovDataLoader.class
OpenIDE-Module-Class: Loader
An example of how to write a DataLoader can be found in the
NetBeans DataLoader Module Tutorial. DataLoaders typically register themselves to support specific
file extensions or mime types.
Unless you are writing support for a language or file-type, typically you
will be using, not creating, DataObjects. Getting the DataObject
for a file is simple: Just call DataObject.find(someFileObject).
DataObjects don't do a lot in and of themselves - that is, it is
almost always a mistake to be casting a DataObject as a particular subclass.
The way to do most interesting interaction with DataObjects is via the method
getCookie(). The pattern, which we will see in more detail in
the section on
Lookup is:
OpenCookie open = someDataObject.getCookie (OpenCookie.class);
open.open();
The above code will actually open a file in the editor. The key here is that,
rather than providing programmatic access to a file's content as a bunch of
instance methods on itself (which would quickly lead to a tangled mess of inheritance issues),
you
ask a
DataObject for an instance of some known interface
that does what you need. This is accomplished by passing a
Class
object to
getCookie(), which will return that object if possible,
or
null if not.
As another example, determining if an opened file has unsaved changes is as
simple as:
boolean needsSaving = someDataObject.getCookie (SaveCookie.class) != null;
Modules can provide their own public interfaces, and make instances of those
objects available via
getCookie(). So, for example, a
DataObject
for an XML file might make a DOM tree or some other structural representation
of the file available via
getCookie() for other modules to
use to manipulate the file's contents. Some common interfaces modules will
typically use via
getCookie() can be found in the package
org.openide.cookies.
Note that the term "cookie" in this context has nothing to do
with the web browser concept of cookies.
Putting it Together: Why .instance Files Work
To illustrate the power of loaders and
DataObjects, recall that
loaders are registered against a file type. And recall that modules can
install actual Java objects via
.instance files. What's
going on here?
What is actually happening is that the very same infrastructure (DataLoaders) that lets
NetBeans recognize a user's .java file on disk and create an appropriate
DataObject is what recognizes .instance files - after
all, the System Filesystem is a filesystem too. There is simply a DataLoader
registered in the system that claims all files with the .instance
extension.
Under the hood, what's really happening is that the DataObject
for a .instance file provides an InstanceCookie.
So to get the actual object in question manually, you would do something
like this:
FileObject file = Repository.getDefault().getDefaultFileSystem().findResource (
"someFolder/com-foo-mymodule-MyClass.instance");
DataObject dob = DataObject.find (file);
InstanceCookie cookie = (InstanceCookie) dob.getCookie (InstanceCookie.class);
MyClass theInstance = (MyClass) cookie.instanceCreate();
Nodes: The Presentation Layer
You've probably noticed that there are quite a few tree components in
NetBeans - the Files and Projects tabs, and others. The
Nodes API is what
provides the contents to those trees. Think of
DataObjects as being
the data model; a Node is where interacting with the user comes in.
A Node provides human-visible things like an icon and a
(possibly localized) display name to DataObjects. And a Node provides a
list of Actions that can appear in a popup menu for that node.
Nodes define context for NetBeans - at any given moment,
there is usually one or more activated nodes which determine what
menu and toolbar actions are enabled - they are the clue to the rest of the
system as to what the user is doing. Each UI component (such as the Files tab
or the Editor) provides an array of Nodes which are activated -
selected. In a tree component, it is rather obvious how this works; but even
when editing in the editor, the activated node triggers what actions are
enabled, depending on where the caret is - if the caret is inside the body
of a method, the activated node is actually the same node you would find
if you expanded the structure tree of that java class in the Projects tab.
So, to get the Node corresponding to a DataObject, simply call
someDataObject.getNodeDelegate().
Nodes, DataObjects and lookup Patterns
Nodes use the same pattern as
DataObject - they have
a
getCookie() method that can be used as described above.
Nodes that represent
DataObjects will typically
delegate to their
DataObject's
getCookie() method.
With Node, it is common to see a second form of this call:
Node.getLookup().lookup (SomeClass.class). The latter is a
newer idiom, which will eventually replace getCookie() in both
Nodes and DataObjects. The specific reason is
that getCookie() requires that the returned object implement
an empty marker interface, Node.Cookie, which unnecessarily
limits what can be returned by getCookie(). The only thing you
need to remember is that the two are functionally equivalent, and in new
code, use getLookup().lookup() where possible. There is further
discussion of what Lookup is below.
Note that all Nodes do not represent DataObjects -
the Nodes API is useful in and of itself for creating tree like hierarchies.
There are a number of UI components that can represent a tree of nodes as
trees, combo boxes, lists, etc. - so typically when one needs to display a
UI with a list or tree in it, the natural choice is to use the Nodes API,
and simply create the appropriate component and set the root node appropriately.
A key thing to remember is that Nodes are intended as a presentation layer for
an underlying data model (which might be files on disk, or whatever you want).
If you find you're putting a lot of logic into your Node
subclass, consider that your model is what needs enhancing - Nodes
should be lightweight and simple, and the model should do the heavy lifting.
Lookup
org.openide.util.Lookup is NetBeans' form of
dependency
injection. As with
DataObjects and
FileObjects,
it has two common usages:
- Local lookup - asking an object for an instance of some interface,
as we saw above with
Node.getLookup().lookup (SomeClass.class)
- Global lookup - services - often singleton instances of some
class - can be registered into the default lookup.
The Default Lookup
The default lookup is an instance of
Lookup returned by
calling
Lookup.getDefault(). The OpenAPIs define a number of
abstract service classes which allow you to get an instance of some object
that is of general use - for example,
org.openide.ErrorManager,
used to log errors and exceptions,
or
org.openide.DialogDisplayer, which displays dialogs to
the user. These are typically things that there only needs to be one of
in the system, so they are effectively singleton objects. To get an
instance of
ErrorManager, you could do as follows:
ErrorManager err = (ErrorManager) Lookup.getDefault().lookup (ErrorManager.class);
err.log ("log message");
In practice this code is a little clunky to ask people to write all the time,
so most such abstract classes will have their own method
getDefault()
implemented as:
public abstract class MyService {
public static MyService getDefault() {
MyService result = (MyService) Lookup.getDefault().lookup (MyService.class);
if (result == null) {
result = new TrivialImplementationOfMyService();
}
return result;
}
public abstract void doSomething (...);
}
Modules can register their own objects into the default lookup in one of two
ways - via the Java
provider extension mechanism - putting a file into the
META-INF/services
directory of their module jar, or by putting a
.instance file in
the
Services folder of the System Filesystem.
The preferred mechanism is the provider extension mechanism, and doing this
is extremely simple: To provide your own implementation of ErrorManager,
for example, simply create two folders under the src/ folder
of your module: META-INF/services. In the services/
folder, put a file called org.openide.ErrorManager. That
file will contain one line of text - the name of the class in your module
that should be used - e.g. com.mymodule.MyLog4JErrorManager.
While we won't go into this in detail here, it is also possible to register
multiple instances of an interface into the default lookup, retrieve
all of them and even listen for changes on the result of that query.
A very thorough discussion of Lookup can be found
here.
Summary
The salient points to remember are:
- FileObjects wrap files (and sometimes other things)
- DataObjects wrap FileObjects and understand what's in a file
- You typically don't call methods on a DataObject, you ask it for objects via
getCookie()
- Configuration information is just another filesystem you can get DataObjects out of
- Nodes wrap DataObjects and provide human-displayable information - actions, icons, names
- Nodes are a presentation layer, not the place to put lots of logic
- Lookup is how you get globally registered services
- Lookup is also how you ask individual objects (Nodes, DataObjects, Projects) for the
objects that do real work
Interconverting between Files, DataObjects, FileObjects and Nodes
Very often you may be integrating an external tool that wants to be passed
instances of
java.io.File; also there are many cases where you need to interconvert between
the various types NetBeans offers which in some way or other represent files.
Here are the typical ways to interconvert between all of the above:
Find a file on disk
FileObject f = Repository.getDefault().getDefaultFilesystem().getRoot().getFileObject("some/folder/someFile.txt");
or if something passes you a File...
FileObject f = FileUtil.toFileObject (new File("some/folder/someFile.txt"))
Turn a FileObject into a File (may fail for virtual filesystems)
File f = FileUtil.toFile (someFileObject)
Get the DataObject for a FileObject
DataObject obj = DataObject.find (someFileObject)
Get the FileObject a DataObject represents
FileObject file = someDataObject.getPrimaryFile()
Get the Node that represents a FileObject
Node n = someDataObject.getNodeDelegate()
Get the DataObject a Node represents (if any)
DataObject obj = (DataObject) someNode.getLookup().lookup(DataObject.class)
Other Things Worth Mentioning...
Below we go through two other critical pieces of NetBeans APIs which complete
the basic picture of things modules typically interact with; they don't have
the type of dual-use issues that the previous topics do, but are included for
completeness.
Explorer Views
Nodes provide a hierarchy of objects; the Explorer API provides
Swing UI components that display a
Node and its children. There
are a large variety of Explorer view classes which can variously represent a
hierarchy of
Nodes as a
JList, a
JMenu,
a
JComboBox, a
JTree,
a JTable and more. Typically when you want to display some hierarchical data
structure in NetBeans, you locate or implement the appropriate Node, create
an appropriate Explorer component for it, and set the Explorer view's root
node to be the node you want to display.
In older versions of NetBeans, the place where the Files and Projects tabs
live was a separate window with the title "Explorer" - you will see
the phrase "open in the Explorer" in older documentation.
The Window System
The API of the Window System is found in
org.openide.windows. A
basic overview is that in NetBeans, you don't deal with
JFrames or
JDialogs - rather, you supply components which are displayed,
and NetBeans window management system decides where and how they appear in
terms of top-level frames. The main thing to know is that all components in
NetBeans are subclasses or usages of
org.openide.windows.TopComponent.
TopComponent has relatively self-explanatory methods such as
open() and
requestActive().
TopComponents
live in
docking modes (the somewhat confusingly named
org.openide.windows.Mode). A
Mode is a container for
multiple
TopComponents - a thing that has Tabs.
Mode
itself is not a GUI component, it is an abstract class that acts as a controller.
TopComponents can be instantiated and opened on the fly, but typically
a module installs its UI components via several XML files inside its jar file and
pointers to those files in the module's XML layer file. Fairly comprehensive
examples of usage can be found in the NetBeans source base in
platform/samples/window-system-*/.
When You're Wondering Where Something is Implemented
Sometimes you just want to go read the code - but it's a jungle of jars out there.
Here are some of the things people often want to track down - the locations
are the actual directories in a checkout of NetBeans sources:
- Where are the standard menus defined? - core/ui
- Where is dialog and windowing handled? - core/windows
- Where is the tab control NetBeans uses for tabs? - core/swing/tabcontrol
- What sets the fonts for NetBeans? - core/swing/plaf