NbmIdioms

Contents

NetBeans APIs in a Nutshell

This overview will quickly familiarize you with how NetBeans modules interact with the NetBeans Platform and with each other. It is not intended as a comprehensive document—the NetBeans API List, the NetBeans Platform Learning Trail, and the video series "Top 10 NetBeans APIs" go into greater detail—but should serve as a guide to understanding the basic concepts of NetBeans module development.

The key to understanding the NetBeans Platform is to realize that very often 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.

FileSystems

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

What FileSystems Are Used For

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.

FileObject text = FileUtil.toFileObject(new File("/.../myFile.txt"));

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 the NetBeans Platform 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 = FileUtil.getConfigFile("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, 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>

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.

DataLoaders and DataObjects

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.

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).

Using DataObjects

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 getLookup(). The pattern, which we will see in more detail in the section on #Lookup is:

OpenCookie open = someDataObject.getLookup().lookup(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 lookup(), 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.getLookup().lookup(SaveCookie.class) != null;

Modules can provide their own public interfaces, and make instances of those objects available via lookup. So, for example, a DataObject for an XML file might make a DOM tree or some other structural representation of the file available via lookup for other modules to use to manipulate the file's contents. Some common interfaces modules will typically use via lookup 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 = FileUtil.getConfigFile("someFolder/com-foo-mymodule-MyClass.instance");
DataObject dob = DataObject.find(file);
InstanceCookie cookie = dob.getLookup().lookup(InstanceCookie.class);
MyClass theInstance = (MyClass) cookie.instanceCreate();

or more simply:

MyClass theInstance = FileUtil.getConfigObject("someFolder/com-foo-mymodule-MyClass.instance");

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 getLookup() method that can be used as described above. Nodes that represent DataObjects will typically delegate to their DataObject's getLookup() method.

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 the "service locator" and "adapter" patterns. 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 NetBeans APIs 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.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 DialogDisplayer, you could do as follows:

DialogDisplayer d = Lookup.getDefault().lookup(DialogDisplayer.class);
d.notify(...);

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 = 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 using the @ServiceProvider annotation.

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 getLookup().lookup(...)
  • 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 = 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 = 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.

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? - o.n.swing.tabcontrol
  • What sets the fonts for NetBeans? - o.n.swing.plaf
Not logged in. Log in, Register

By use of this website, you agree to the NetBeans Policies and Terms of Use. © 2012, Oracle Corporation and/or its affiliates. Sponsored by Oracle logo