This chapter illustrates concepts and APIs used to manage, manipulate, and represent data. Upon completion, you will be able to handle and display all forms of data in a professional way. The NetBeans Platform uses the same approach for its internal data, such as the data in the System Filesystem.
The NetBeans Platform provides comprehensive solutions for creating, managing, manipulating, and presenting data. Solutions are provided by the File Systems, Data Systems, and Nodes APIs.
Each of these APIs can be found on its own abstraction layer. In combination with specific data outside a NetBeans Platform application, this system is divided into four layers, as shown in Figure 7-1.
A data system is initially abstracted using the FileSystem
class. The FileSystem
class lets users address physical data from different sources in the same way. Examples of this are the local file system, a file system organized as an XML file (the System Filesystem is built accordingly—see Chapter 3), or even a JAR file.
To use it, simply provide the desired implementation of the abstract FileSystem
class. This is how the File Systems API abstracts specific data and provides a virtual file system giving shared data access to the whole application.
All access is, in that way, completely independent of its origin. However, abstracted data on the abstraction layer in the form of a FileObject
class does not contain any information about the kind of data it handles. Moreover, this layer contains no data-specific logic.
Just above this layer is found the Data Systems API, on the logical layer. That locates objects representing data of specific types and is built on top of the DataObject
class. A DataObject.Factory
creates objects for every desired data type. The upper layer in this concept is the Nodes API. This API is located on the presentation layer. A node is responsible for type-specific representation of data in the user interface. As a result, a node represents a DataObject
and is responsible for the creation of the presentation layer.
The NetBeans Platform gives transparent access to files and folders by providing the File Systems API. This API allows abstract access to files that are always handled in the same way, independently, as if the data is stored in the form of a virtual XML file system, such as that used by the System Filesystem, a JAR file, or in a normal folder.
General interfaces of a file system are specified in the abstract class FileSystem
. The abstract class AbstractFileSystem
is used as a superclass for specific file system implementations. The specific implementations LocalFileSystem, JarFileSystem
, and XMLFileSystem
use this class as their superclass (see Figure 7-2). The class MultiFileSystem
further provides a proxy for multiple file systems and is also used here as superclass.
Data inside the file system, such as folders and files, is represented by a FileObject
class. This is an abstract wrapper class for the standard java.io.File
class. The implementation of a FileObject
is provided by the specific file system. The FileObject
class provides, along with these standard operations, the ability to listen to changes to folders and files.
Following is an outline of operations provided by the FileObject
class.
To obtain a FileObject
for an existing file on the local file system, use the helper class FileUtil
:
FileObject obj = FileUtil.toFileObject(new File("C:/file.txt"));
To create a FileObject
from a specific FileSystem
object, call the findResource()
method, using the full path:
FileSystem fs = Repository.getDefault().getDefaultFileSystem(); FileObject obj = fs.findResource("folder/file");
This is the manner in which you create a FileObject
for data located in the System Filesystem, which is an XMLFileSystem
.
The following allows you to create new files or folders using FileObject
s:
File file = new File("E:/newfolder/newfile.txt"); File folder = new File("E:/newfolder2"); FileObject fo1 = FileUtil.createData(file); FileObject fo2 = FileUtil.createFolder(folder);
If you already have a FileObject
folder, create a file or folder in the corresponding file system as follows:
FileObject folder = ... FileObject file = folder.createData("newfile.txt"); FileObject subfolder = folder.createFolder("newfolder");
To rename a folder or file, make sure someone else is not editing it at the same time, by using a FileLock
object. This lock is released after renaming, using a finally
block.
FileObject myfile = ... FileLock lock = null; try { lock = myfile.lock(); } catch (FileAlreadyLockedException e) { return; } try { myfile.rename(lock, "newfilename", myfile.getExt()); } finally { lock.releaseLock(); }
Deleting folders or files is straightforward. The delete()
method takes care of reserving and releasing a FileLock
. Consequently, deleting only requires the following line:
FileObject myfile = ... myfile.delete();
A variant of the delete()
method is available that enables users to pass their own file FileLock
, analogous to the renaming of a FileObject
.
A FileObject
cannot be moved in the same way as a File
, simply by renaming. Instead, this is achieved by using the method moveFile()
, provided by the class FileUtil
. It allows moving FileObject
s and handles copying files or folders to the destination folder, deleting the source and automatically allocating and releasing the required FileLock
objects.
FileObject fileToMove = ... FileObject destFolder = ... FileUtil.moveFile(fileToMove, destFolder, fileToMove.getName());
Reading and writing FileObject
s is done, as usual in Java, by streams. The FileObject
class provides the methods InputStream
and OutputStream
for this purpose. We wrap these for reading in a BufferedReader
and for writing in a PrintWriter
, as shown in Listing 7-1.
Example 7.1. Reading and writing a FileObject
FileObject myFile = ... BufferedReader input = new BufferedReader( new InputStreamReader(myFile.getInputStream())); try { String line = null; while((line = input.readLine()) != null) { // process the line } finally { input.close(); } PrintWriter output = new PrintWriter( myFile.getOutputStream()); try { output.println("the new content of myfile"); } finally { output.close(); }
You optionally pass your own FileLock
to the method getOutputStream()
.
To monitor changes, register a FileChangeListener
for the FileObject
that responds to data changes inside the file system (see Listing 7-2).
Example 7.2. Responding to changes to a DataObject
File file = new File("E:/NetBeans/file.txt"); FileObject fo = FileUtil.toFileObject(file); fo.addFileChangeListener(new FileChangeListener(){ public void fileFolderCreated(FileEvent fe) { } public void fileDataCreated(FileEvent fe) { } public void fileChanged(FileEvent fe) { }
public void fileDeleted(FileEvent fe) { } public void fileRenamed(FileRenameEvent fre) { } public void fileAttributeChanged(FileAttributeEvent fae) { } });
The methods fileFolderCreated()
and fileDataCreated()
, called when a file or folder is created, only make sense when the monitored FileObject
is a folder.
While changing files, the event is always processed for the file itself, as well as its parent directory. This means that you will be informed of changes of files, even when only monitoring the parent directory. If you are not interested in all the events of the FileChangeListener
interface, use the adapter class FileChangeAdapter
instead.
The classes FileSystem, FileObject
, and FileUtil
provide several additional and helpful methods. It is worth having a closer look at the documentation of the File Systems API.
The Data Systems API provides a logical layer on top of the File Systems API. While a FileObject
manages its data regardless of type, a DataObject
is a wrapper for a FileObject
of a quite specific type. A DataObject
extends a FileObject
with type-specific features and functionalities. These are specified through interfaces or abstract classes: so-called cookies. Their implementations are published by the DataObject
using the local Lookup.
By this means, the capabilities of a DataObject
can be flexibly adjusted and accessed from outside. Due to the fact that a DataObject
knows the type of its managed data, it is able to provide specific data accordingly. That means a DataObject
is responsible for the creation of a Node
object, representing data in the user interface. Creating a DataObject
is done by a DataObject.Factory
.
The Data Systems API provides a set of superclasses (Figure 7-3), allowing easy implementation of DataObject
s and data types used in your application.
The best way to imagine the combination of the three systems is by means of an example that shows how APIs of the three layers cooperate and build upon each other. The NetBeans IDE provides a wizard that creates parts of the system for a file, all in one go.
Now we will use this wizard to add a DataObject
for MP3 files to the module created in Chapter 3. To achieve this, go to File
Optionally, provide more than one prefix by entering different types separated by commas. This is meaningful for MPEG video files when you enter, for example, mpg, mpeg.
Click Next to enter the last page of the wizard. Enter Mp3 as the class name prefix and choose a supporting icon with a size of 16×16 pixels. When ready, click Finish, which initiates the creation of the file type. This creates the Mp3DataObject
class containing a FileObject
of type .mp3
, and registers a default DataObject.Factory
in the layer file to create the DataObject
. We inspect these components in the following sections, using an example. Also presented are features that go beyond the initial examples.
In principle, a DataObject
is specified by the abstract class DataObject
, but typically MultiDataObject
is used as a superclass. This class already implements most of the methods in DataObject
. On one hand, this results in a very small class, but on the other, as the name MultiDataObject
already suggests, it contains more than one FileObject
.
A DataObject
always owns the FileObject
as its primary file. A MultiDataObject
contains one or more additional FileObject
s, which are called secondary files. Secondary files are typically used for dependent files, as in the Form Editor, where a single DataObject
represents the files myform.java, myform.form
and myform.class
. Here, myform.java
is the primary file, and the files myform.form
and myform.class
are the secondary files.
A FileObject
inside a DataObject
is handled using the class MultiDataObject.Entry
, whereas the subclass FileEntry
is used in most cases. This class processes such standard file operations as moving or deleting. Have a look at the class Mp3DataObject
in Listing 7-3, generated by the wizard.
Example 7.3. DataObject class for an MP3 FileObject. This class provides the logic for MP3 data.
public class Mp3DataObject extends MultiDataObject { public Mp3DataObject(FileObject pf, MultiFileLoader loader) throws DataObjectExistsException, IOException { super(pf, loader); } @Override protected Node createNodeDelegate() { return new DataNode(this, Children.LEAF, getLookup()); } @Override public Lookup getLookup() { return getCookieSet().getLookup(); } }
As you already know, DataObject
s are typically created by a DataObject.Factory
. The constructor of the class Mp3DataObject
requires passing both the primary file, which contains the real MP3 file, and the MultiFileLoader
(DataLoader
subclass) responsible for this DataObject
. We simply pass these parameters to the base class constructor, which handles management of the DataObject
.
Since a DataObject
knows its data type, it is also responsible for creating the corresponding node used to display the DataObject
for the user interface. This is done by means of the factory method createNodeDelegate()
, creating and returning an instance of the Node
class DataNode
. This is the interface to the Nodes API, located in the presentation layer (see Figure 7-1, shown previously). We will revisit this in the "Nodes API" section.
The main difference between a FileObject
and a DataObject
is that DataObject
s know the data they contain. This means a DataObject
distinguishes itself by the fact that it can provide properties and functionalities for specific types of data; in our case an MP3 file.
The functionality provided by a DataObject
for its data is specified by its interfaces or abstract classes. These are called cookies. Instances of these interfaces are managed by the DataObject
, using a CookieSet
. The fact that the interfaces need not be implemented by the DataObject
itself, but are managed using the CookieSet
, allows a DataObject
to dynamically provide its functionality.
For example, this means it can provide an interface to allow stopping a currently playing MP3 file, only available while the MP3 file is played. In this way, it is possible to flexibly extend a DataObject
with new functionalities. Type-safe access to these interfaces is obtained via the Lookup provided by the DataObject
using the getLookup()
method.
The structure of the Mp3DataObject
is now finished. The constructor obtains the FileObject
that is managed from the MultiFileLoader
in the adjacent abstraction layer. The DataObject
supplies a representation for the overlying presentation layer and, finally, reveals its functionality to the environment via Lookup. The superclasses DataObject
and MultiDataObject
provide several methods for using DataObject
s.
Looking through the API documentation is very helpful when trying to understand these classes.
First, specify the functionality of the Mp3DataObject
by an interface. Call it PlayCookie
and specify the method play()
, used to play the corresponding Mp3DataObject
:
public interface PlayCookie { public void play(); }
Also required is an implementation of functionality specified by our cookie—possibly the direct implementation of the interface by the class Mp3DataObject
. Better is to use a separate class, a so-called support class. This permits flexibly adding and removing functionality to and from the Mp3DataObject
. Semantic grouping of multiple cookies is possible as well, and the Mp3DataObject
class remains lean.
public class PlaySupport implements PlayCookie { private Mp3DataObject mp3 = null; public PlaySupport(Mp3DataObject mp3) { this.mp3 = mp3; } public void play() { //Mp3Player.getDefault().play(mp3); System.out.println("play file"); } }
Now, adding an instance of this support class to the CookieSet
of the Mp3DataObject
class is required. This is accomplished using the method getCookieSet()
and adding an instance of the class PlaySupport
via assign()
. Here, we also set the type to PlayCookie
, although you can use the PlaySupport
class. But this method allows us independence from the implementation.
public class Mp3DataObject extends MultiDataObject { public Mp3DataObject(FileObject pf, MultiFileLoader loader) throws DataObjectExistsException, IOException { super(pf, loader); getCookieSet().assign(PlayCookie.class, new PlaySupport(this)); } }
We have now added functionality to the Mp3DataObject
such that it can be used outside the local Lookup available via getLookup()
.
A question remains concerning access to the functionalities of a DataObject
. This is answered by a CookieAction
class. Use the class MyCookieAction
created in Chapter 4 or create a new class via the wizard with File
The method cookieClasses()
is implemented as follows:
protected Class[] cookieClasses() { return new Class[] { Mp3DataObject.class }; }
As shown in Chapter 4, the performAction()
method of the CookieAction
receives selected nodes. As you have seen in the Mp3DataObject
's createNodeDelegate()
method, the node representing the Mp3DataObject
receives the local Lookup from the Mp3DataObject
. This Lookup allows us querying the functionalities of the Mp3DataObject
. In other words, the DataNode
is a proxy for the Mp3DataObject
. It is precisely that Lookup that we obtain from the selected node via the performAction()
method. As usual, we now use the Lookup, via the getLookup()
method, to obtain an instance of PlayCookie
, after which we execute its play()
method.
protected void performAction(Node[] nodes) { PlayCookie pc = nodes[0].getLookup().lookup(PlayCookie.class); pc.play(); }
Test the code using the Favorites
module. Make sure the Favorites
module is active in the application by going to Properties
By means of the example in Listing 7-4, the provided functionalities of an Mp3DataObject
can be changed at runtime, which also implicitly control the actions available to the user. Previously, we created a cookie and a support class that plays an MP3 file. Now we create another to stop an MP3 file. Set the current play status of the Mp3DataObject
using the method playing()
.
Example 7.4. Cookies and support classes required to play an Mp3DataObject
public interface PlayCookie { public void play(); } public class PlaySupport implements PlayCookie { private Mp3DataObject mp3 = null; public PlaySupport(Mp3DataObject mp3) { this.mp3 = mp3; } public void play() { System.out.println("play"); mp3.playing(true); } } public interface StopCookie { public void stop(); } public class StopSupport implements StopCookie { private Mp3DataObject mp3 = null; public StopSupport(Mp3DataObject mp3) { this.mp3 = mp3; } public void stop() { System.out.println("stop"); mp3.playing(false); } }
We use the constructor to create both support classes. We can assume that the MP3 file is not being played, and we assign the PlaySupport
class to the CookieSet
. Using the playing()
method, called by the support classes, we change the cookies available in the CookieSet
. If the file is currently being played, all instances of PlayCookie
are removed by passing the type without any instances to the assign()
method, and adding an instance of StopCookie
(see Listing 7-5). If the file is stopped, everything happens in reverse order.
Example 7.5. Adding and removing cookies dynamically
public class Mp3DataObject extends MultiDataObject { private PlaySupport playSupport = null; private StopSupport stopSupport = null; public Mp3DataObject(FileObject pf, MultiFileLoader loader) throws DataObjectExistsException, IOException {
super(pf, loader); playSupport = new PlaySupport(this); stopSupport = new StopSupport(this); getCookieSet().assign(PlayCookie.class, playSupport); } public synchronized void playing(boolean value) { if(value) { getCookieSet().assign(PlayCookie.class); getCookieSet().assign(StopCookie.class, stopSupport); } else { getCookieSet().assign(StopCookie.class); getCookieSet().assign(PlayCookie.class, playSupport); } } }
Making the example complete requires two additional action classes used to start and stop the MP3 file. These are two CookieAction
classes, using PlayCookie
and StopCookie
as cookie classes (see Listing 7-6 for the PlayAction
class). The menu (or toolbar) entries are activated or deactivated automatically, depending on which cookie or support class is currently provided by the selected MP3 file.
Example 7.6. Context-sensitive actions that become active as soon as the selected Mp3DataObject provides an instance of the corresponding cookie
public final class PlayAction extends CookieAction { protected void performAction(Node[] n) { PlayCookie pc = n[0].getLookup().lookup(PlayCookie.class); pc.play(); } protected Class[] cookieClasses() { return new Class[] { PlayCookie.class }; } } public final class StopAction extends CookieAction { protected void performAction(Node[] n) { StopCookie sc = n[0].getLookup().lookup(StopCookie.class); sc.stop(); } protected Class[] cookieClasses() { return new Class[] { StopCookie.class }; } }
Normally, a DataObject
need not be created explicitly, but is created by the DataLoader
pool on demand. Optionally, you can create a DataObject
for a given FileObject
using the static find()
method of the DataObject
class:
FileObject myFile = ... try { DataObject obj = DataObject.find(myFile); } catch(DataObjectNotFoundException ex) { // no loader available for this file type }
If no DataLoader
or DataObject.Factory
is registered for the given file, a DataObjectNotFoundException
is thrown.
New since NetBeans Platform 6.5 is the concept of DataObject
factories, which can be registered declaratively in the layer file. There is no longer a need for a separate DataLoader
class. In most cases, you can use the default factory implementation provided by the DataLoaderPool
class. A DataObject
factory must implement the new interface DataObject.Factory
.
In our case, the factory is named Mp3DataLoader
and is registered in the folder Loaders/audio/mpeg/Factories
by the File Type wizard (see Listing 7-7). This factory is a default implementation created by the DataLoaderPool.factory()
method specified via the instanceCreate
attribute. This method needs the class of the data object to create, the MIME type associated with the object, and an icon to use by default for nodes representing data objects created with this factory.
Example 7.7. DataObject factory registration in layer file
<folder name="Loaders"> <folder name="audio"> <folder name="mpeg"> <folder name="Factories"> <file name="Mp3DataLoader.instance"> <attr name="SystemFileSystem.icon" urlvalue="nbresloc:/com/galileo/netbeans/module/mp3.png"/> <attr name="dataObjectClass" stringvalue="com.galileo.netbeans.module.Mp3DataObject"/> <attr name="instanceCreate" methodvalue="org.openide.loaders.DataLoaderPool.factory"/> <attr name="mimeType" stringvalue="audio/mpeg"/> </file> </folder> </folder> </folder> </folder>
In the previous section, you saw that there is no need for a special DataLoader
implementation for your file type. Although the usage of the default DataObject
factory is the recommended approach to take, we will have a look at the DataLoader
classes and how you can create your own loader.
A DataLoader
is a factory for a DataObject
and is responsible for exactly one data type. The DataLoader
recognizes the type of the file, either by file extension or XML root element. DataLoader
s are implemented by modules and registered via the layer file. This registration can be used to find the corresponding DataLoader
for an appropriate file type. Basically, there exist two types of DataObject
s: a DataObject
representing only one file, the primary file; and a DataObject
that contains additional files, the so-called secondary files. Depending on which data type used, you need different types of DataLoader
s.
Using a DataObject
with only one primary file, as in our MP3 example, select a DataLoader
of the type UniFileLoader
. Whenever DataLoader
s need to load additional secondary files, choose a MultiFileLoader
. The UniFileLoader
is only a special variant of the MultiFileLoader
, and its created objects are of the type MultiDataObject
. You can directly derive from DataLoader
and DataObject
, but it is considerably easier to use the MultiFileLoader
or UniFileLoader
class.
The DataLoader Mp3DataLoader
responsible for the creation of DataObject
s of the type Mp3DataObject
was created by the NetBeans wizard. It inherits the abstract superclass UniFileLoader
, since our Mp3DataObject
solely represents an MP3 file or FileObject
. Observe the assembly and functionality of a DataLoader
by means of the Mp3DataLoader
(see Listing 7-8).
A DataLoader
is responsible for a specific MIME type. First, define this type by a private data element. Use the type audio/mpeg
. Pass the complete name and DataObject
class for which the DataLoader
is responsible to the base class constructor. This representation class is labeled for our example as com.galileo.netbeans.module.Mp3DataObject
.
Setting a displayable name for the DataLoader
is done by overriding the method defaultDisplayName()
, in which we read the name from a resource bundle. The initialize()
method is used to call the method of the base class and afterward to add the MIME type to the ExtensionList
, accessed via the getExtensions()
method. The DataLoader
infers the file extension from the MIME type, using the file Mp3Resolver.xml
, created by the wizard and registered in the layer file in the Services/MIMEResolver
folder. If you don't register the MIME type to the ExtensionList
using addMimeType()
, you can pass the file name directly via addExtension()
, making the file Mp3Resolver.xml
and its entry in the layer file obsolete.
Registration of file name extensions or MIME types in an ExtensionList
allows the DataLoader
to recognize its responsibility for a specific file type. Checking if a DataLoader
comes into consideration for a specific file type is done by findPrimaryFile()
. The UniFileLoader
, possessing the ExtensionList
, already implements this method and checks if a MIME type or the extension of the parent file matches the type of DataLoader
.
If you want to use a MultiDataLoader
, you need to implement the method yourself, and should first check if the passed FileObject
is the primary file. In this case, directly return it; otherwise (if it is a secondary file), you have to find the appropriate primary file.
For that purpose, the method FileUtil.findBrother()
is useful. Otherwise, the findPrimaryFile()
method will return null
, indicating the DataLoader
is not responsible for the file type. The method createMultiObject()
is the DataLoader
's factory method, responsible for creating an Mp3DataObject
instance. This method is called with the primary file as a parameter (in this case an MP3 file), which we pass to the Mp3DataObject
constructor. For the creation of MultiDataObject.Entry
instances, which manage the FileObject
s inside the DataObject
, the responsible methods are createPrimaryEntry()
and createSecondaryEntry()
. For a UniFileLoader
, only the createPrimaryEntry()
method, which is already implemented, is needed. If you use a MultiFileLoader
, you must implement both methods inside the subclass.
Last, a DataLoader
defines a folder in the System Filesystem—the layer file in which actions are registered for the particular data type—using the method actionsContext()
.
These registered actions are shown in the node's context menu—that is, in the context menu of the node responsible for the presentation of the DataObject
.
Example 7.8. DataLoader for the creation of DataObjects
public class Mp3DataLoader extends UniFileLoader { public static final String REQUIRED_MIME = "audio/mpeg"; public Mp3DataLoader() { super("com.galileo.netbeans.module.Mp3DataObject"); } protected String defaultDisplayName() { return NbBundle.getMessage(Mp3DataLoader.class, "LBL_Mp3_loader_name"); } protected void initialize() { super.initialize(); getExtensions().addMimeType(REQUIRED_MIME); } protected MultiDataObject createMultiObject(FileObject pf) throws DataObjectExistsException, IOException { return new Mp3DataObject(primaryFile, this); } protected String actionsContext() { return "Loaders/" + REQUIRED_MIME + "/Actions"; } }
As already mentioned, with the introduction of the DataObject.Factory
interface, the DataLoader
superclass also implements this interface. This enables you to register your own DataLoader
implementation in the layer file instead of the manifest file. The order of the loaders can be determined by the generic position
attribute (see also Chapter 3).
The preceding Mp3DataLoader
can be registered as Listing 7-9 shows.
Example 7.9. DataLoader registration in layer file
<folder name="Loaders"> <folder name="audio"> <folder name="mpeg"> <folder name="Factories"> <file name="com-galileo-netbeans-module-Mp3DataLoader.instance"> <attr name="position" stringvalue="100"/> </file> </folder> </folder> </folder> </folder>
With this entry, you have registered your own DataLoader
or DataObject.Factory
implementation, which creates Mp3DataObject
s.
The Nodes API is the third and uppermost layer in the NetBeans Resource Management System. The role of the Nodes API is visual representation of data. Closely connected to this API is the Explorer & Property Sheet API, which is the container and manager of nodes. A node exists to present data to the user interface of an application, as well as to give the user actions, functionality, and properties for interacting with underlying data.
However, a node need not merely present data, since it can be used for many other things as well. For example, an action hiding beneath a node could be invoked when the node is double-clicked. Be aware that a node is not typically concerned with business logic, but focuses on providing a presentation layer, delegating user interaction to action classes and, where applicable, to its related DataObject
.
The general interfaces and behavior are defined by the Node
superclass. All subclasses can be displayed and managed by an explorer view. Possible subclasses of the Node
class are shown in Figure 7-5.
The classes AbstractNode
and FilterNode
derive from the Node
class. The AbstractNode
provides the simplest form of the Node
class. Use this class to instantiate a new Node
directly, without needing to implement or extend any of the Node
classes. On the other hand, the FilterNode
creates a proxy Node
that delegates its method calls to the original Node
. This kind of Node
is used when data needs to be displayed in different ways.
The BeanNode
is used to represent a JavaBean
. Another kind of Node
, the IndexedNode
, lets its children be organized based on a given index. Finally, we have the subclass DataNode
, which is most commonly used when representing data from files. The DataNode
is the Node
type representing DataObject
s such as those you learned about in the previous sections.
While in previous NetBeans Platform versions, the File Type wizard created a special DataNode
implementation for your file type, there is in general no more need for such a class. This class has only set the icon of the file type. This is now done by the registration of the DataObject
factory. Instead, a DataNode
instance is used.
Nevertheless, there are use case where you need your own Node
implementation, as you will see in Chapters 9 and 18 (for example, if the node shall provide properties to be displayed in the Properties window).
Each Node
object has its own Children
object, providing a container for child nodes, which are the node's subnodes. The Children
object is responsible for the creation, addition, and structuring of child nodes. Each node within the Children
object has the Node
that owns the Children
object as its parent. For nodes that do not have their own children, such as our DataNode
for MP3 files, we pass in Children.LEAF
as an empty container.
Several variations of the Children
object derive from the Children
superclass, as shown in Figure 7-6.
Table 7-1 shows the different container classes, their characteristics, and their uses.
Table 7.1. Different Children container class variations and their uses
Class | Use |
---|---|
| Superclass for all other |
| Typical superclass for your implementation. Nodes are connected with a key. These keys are also used for ordering. |
| The nodes are stored in a |
| Extends the |
| Extends the |
A node makes a context menu available to the user, allowing context-sensitive actions. A DataNode
obtains its context menu's actions from the DataLoader
of the DataObject
it represents. For this purpose, a DataLoader
defines a MIME-specific folder in the layer file via the method actionsContext()
, where actions are registered. These are read and added automatically to the context menu.
For nodes that do not represent DataObject
s, the getActions()
method in the Node
is used to define the actions in the context menu. Override this method in your class to add more actions to the set provided by default by the NetBeans Platform. Add actions programmatically or use Lookup to retrieve them from the layer file (see the "Context Menu" section in Chapter 5). When overriding the getActions()
method, make sure to add a call to super.getActions()
, in addition to the actions you add to the set.
You can also override the getPreferredAction()
method, which provides the action invoked when the user double-clicks the node. If you return null
from this method, the first action from the getActions()
array is invoked.
To react to Node
events, use a PropertyChangeListener
, as well as a NodeListener
. Use the PropertyChangeListener
to be informed of changes to Node
properties provided via the getPropertySet()
method. Via the NodeListener
, you can listen to internal node changes, such as changes to the name, the parent node, and the child nodes. To that end, the Node
class makes a range of property keys available, such as PROP_NAME
and PROP_LEAF
. The methods listed in Table 7-2 are offered by the NodeListener
.
Table 7.2. Methods of the NodeListener interface
Method | Event |
---|---|
| Called when child nodes are added |
| Called when child nodes are removed |
| Called when child nodes are reordered |
| Called when the parent node is destroyed |
| Called when a node property, such as its name, is changed |
If you don't want to be informed of the events, or you don't want to implement them, use the NodeAdapter
class instead of the NodeListener
interface.
As an example, we introduce the Nodes API, together with the Data Systems API beneath it. You'll learn how to create your own nodes and how to build the Children
container beneath them. To that end, the nodes are presented in a tree hierarchy, representing actions registered in the layer file. By doing so, we allow the tree structure to be extended, creating an extension point for other modules. To present the nodes in a tree structure, we use the Explorer & Property Sheet API, about which you will learn more in the next section. The completed example shows an explorer view, as depicted in Figure 7-7.
We define content of the explorer view in the layer file, in a folder called Explorer
. That folder defines the extension point of our window and, more generally, the extension point of our module. Within the folder, actions are registered at various levels of nesting, displayed in the explorer view represented by nodes. The content of the layer file, as reflected in Figure 7-7, is as shown in Listing 7-10.
Example 7.10. Extension point in the layer file. All entries in the Explorer folder are displayed in the Explorer window
<folder name="Explorer"> <attr name="icon" stringvalue="com/galileo/netbeans/module/explorer.png"/> <folder name="MP3 Player"> <attr name="icon" stringvalue="com/galileo/netbeans/module/player.png"/> <file name="PlaylistAction.shadow"> <attr name="originalFile" stringvalue=" Actions/Edit/com-galileo-netbeans-module-PlaylistAction.instance"/> </file> </folder> <folder name="Views"> <attr name="icon" stringvalue="com/galileo/netbeans/module/views.png"/> <file name="OutputAction.shadow"> <attr name="originalFile" stringvalue="Actions/Window/ org-netbeans-core-output2-OutputWindowAction.instance"/> </file> </folder> <folder name="Favorites"> <attr name="icon" stringvalue="com/galileo/netbeans/module/favorites.png"/> <file name="FavoritesAction.shadow"> <attr name="originalFile" stringvalue=" Actions/Window/org-netbeans-modules-favorites-View.instance"/> </file>
</folder> </folder>
You can see that actions are registered in the same way as is done for menus or toolbars, via shadow files. Additionally, we will assign an icon to a folder with the self-defined attribute icon
.
To display this structure in a node hierarchy, we require a Node
class that represents a folder, a Children
class to manage all the nodes beneath a folder (as well as all the actions and subfolders), and a Node
class that represents an action.
Start with the Node
class that represents the content of a folder. We call this Node
the ExplorerFolderNode
and create it from the convenience class AbstractNode
. As a result, we need nothing more than a constructor. Pass a FileObject
into the constructor, representing a folder within the layer file. The superclass constructor receives an instance of the Children
class ExplorerNodeContainer
, representing all the child nodes. Then set the name and icon path of the node from values in the layer file.
public class ExplorerFolderNode extends AbstractNode { public ExplorerFolderNode(FileObject node) { super(new ExplorerNodeContainer(node)); setDisplayName(node.getName()); String iconBase = (String) node.getAttribute("icon"); if(iconBase != null) { setIconBaseWithExtension(iconBase); } } }
The ExplorerNodeContainer
container is derived from the Children<Keys>
class normally treated as a superclass. Pass the parent node's FileObject
into the constructor, since this is where entries found beneath this node are loaded and used.
The addNotify()
method is called automatically when the parent node is expanded. That means the children are created on demand—that is, only when needed. Within the addNotify()
method, we set the key with the FileObject
of the parent node. This object is then received as a parameter within the createNodes()
method, which is invoked automatically in order to actually create the Children
object.
Within the createNodes()
method, use the getFolders()
method to read all the subfolders, create an instance of ExplorerFolderNode
, and add it to a list (see Listing 7-11). That class contains an ExplorerNodeContainer
, whereby we obtain the required recursion for creating any level of hierarchy required.
Next, read the actions from the folder. Use the FolderLookup
class, which gives the instances. This acts recursively by default, which means it passes back all the actions, not just those wanted from the current folder. Therefore, create a subclass of FolderLookup
named ActionLookup
. To prevent the recursion, simply override the acceptContainer()
and acceptFolder()
methods and return null
. Now use the ActionLookup
to retrieve all instances of the current folder.
Use the lookupAll()
method to receive all instances that are of type Action
. For each Action
, we receive an ExplorerLeafNode
, which is responsible for representing an Action
and not a Children
object, which therefore does not return a lower level of the hierarchy. We add this Node
to the list, which we return as an array. Therefore, this method provides the core of the whole system created in this example.
Example 7.11. Container class that loads and manages all child nodes dynamically
public class ExplorerNodeContainer extends Children.Keys<FileObject> { private FileObject folder = null; public ExplorerNodeContainer(FileObject folder) { this.folder = folder; } protected void addNotify() { setKeys(new FileObject[] {folder}); } protected Node[] createNodes(FileObject f) { ArrayList<Node> nodes = new ArrayList<Node>(); /* add folder nodes / for(FileObject o : Collections.list(f.getFolders(false))) { nodes.add(new ExplorerFolderNode(o)); } DataFolder df = DataFolder.findFolder(f); FolderLookup lkp = new ActionLookup(df); /* add leaf nodes, which represents an action */ for(Action a : lkp.getLookup().lookupAll(Action.class)) { nodes.add(new ExplorerLeafNode(a)); } return(nodes.toArray(new Node[nodes.size()])); } /* non-recursive folder lookup */ private static final class ActionLookup extends FolderLookup { public ActionLookup(DataFolder df) { super(df); } protected InstanceCookie acceptContainer(DataObject.Container con) { return(null); } protected InstanceCookie acceptFolder(DataFolder df) { return(null); } } }
Finally, have a look at the ExplorerLeafNode
class (see Listing 7-12). It simply contains a single action, invoked on the double-click of a node. The action received via the ActionLookup
is passed to the constructor. Since this type of node does not contain child nodes, we pass Children.LEAF
to the superclass constructor. Then we set the name to be used when the node is displayed.
Use the getPreferredAction()
method to provide an action to be invoked when the node is double-clicked. That action will obviously be the one we received as a parameter. Last but not least, override the getIcon()
method to set the icon of the action to be the icon of the node.
Example 7.12. Leaf node that represent the action, executed by a double-click
public class ExplorerLeafNode extends AbstractNode { private Action action = null; public ExplorerLeafNode(Action action) { super(Children.LEAF); this.action = action; setDisplayName(Actions.cutAmpersand((String)action.getValue(Action.NAME))); } public Action getPreferredAction() { return action; } public Image getIcon(int type) { ImageIcon img = (ImageIcon) action.getValue(Action.SMALL_ICON); if(img != null) { return img.getImage(); } else { return null; } } }
In this section, you've seen how to create your own Node
classes, and you've seen which responsibilities are handled by the Children
object. With these classes, we are able to display folders and files within the layer file. To place the node in a tree structure, work with explorer views. That part of the presentation of nodes is handled by the Explorer & Property Sheet API. The next section affords a short introduction into this topic, ending with an example that wraps up the code discussed in this section.
Using the Explorer & Property Sheet API, you display and manage nodes in a wide variety of ways. To that end, the API makes a range of explorer views available, with which you present your nodes in one of many structures. The class hierarchy of these views is shown in Figure 7-8.
The ChoiceView
class displays your node in a combo box, while the MenuView
does so in a menu structure of any depth. The most commonly used view is the BeanTreeView
, displaying nodes in a tree structure. Apart from the display of nodes, the views provide actions and context-sensitive menus via the getActions()
method.
Managing explorer views is done by the ExplorerManager
class. An instance of this class is provided by the TopComponent
containing the explorer view. It is important to note that the ExplorerManager
is not connected to the explorer view via any coding on your part. The explorer view simply examines its component hierarchy until it finds an ExplorerManager
to display it. To enable the ExplorerManager
to be found by the explorer view, the TopComponent
implements the ExplorerManager.Provider
interface. This interface provides the getExplorerManager()
method, by which the ExplorerManager
and the view find each other. As a result, multiple views are displayed by the same ExplorerManager
.
A main task of the ExplorerManager
is maintaining the selection of the nodes in the view. It makes available the selected node, together with the selected node's Lookup. To make the selection available to the outside—either to actions, other TopComponent
s, or completely different modules—we must take additional steps. Use the helper class ExplorerUtils
to define a Lookup using the createLookup()
method, representing the selected node along with the selected node's Lookup. Define this Lookup via the associateLookup()
method as the TopComponent
's local Lookup. As a result, the Lookup is available from the outside, thanks to the global Lookup obtained via Utilities.actionsGlobalContext()
.
In the "Nodes API" section, we created Node
and Children
classes. What remains missing is a window containing the explorer view that displays the node. As you complete this missing step, you'll learn how the explorer view and the ExplorerManager
relate to each other. Start by using the Window Component wizard to create a new TopComponent
named ExplorerTopComponent
. Next, add the ExplorerManager
. Do this by implementing ExplorerManager. Provider
and create a private instance of the ExplorerManager
. Then use getExplorerManager()
to return the ExplorerManager
.
Next, add an explorer view to the TopComponent
. In this case, we add a BeanTreeView
. A simple way of doing this is to drag and drop a JScrollPane
onto the TopComponent
, and then, switching to the Properties dialog, set Custom Code Creation in the Code tab to "new BeanTreeView()." Your initComponents()
method will then look as shown in Listing 7-13. As pointed out earlier, the explorer view finds the ExplorerManager
automatically, which means you need not take extra steps to connect the view to the ExplorerManager
.
Each view and each ExplorerManager
are based on a root element from which all nodes derive. To that end, add setRootContext()
to the initTree()
method and pass in an instance of the ExplorerFolderNode
. From that node, all others are created. We create the node only once the Explorer
folder is available in the System Filesystem—that is, only when a module registers an Explorer
folder in its layer file.
Example 7.13. Explorer window that displays the nodes with a BeanTreeView. An ExplorerManager manages the nodes.
public final class ExplorerTopComponent extends TopComponent implements ExplorerManager.Provider { private static final String ROOT_NODE = "Explorer"; private final ExplorerManager manager = new ExplorerManager(); private ExplorerTopComponent() { initComponents(); initTree(); initActions(); associateLookup(ExplorerUtils.createLookup(manager, getActionMap())); } private JScrollPane jScrollPane1; private void initComponents() { jScrollPane1 = new BeanTreeView(); setLayout(new BorderLayout()); add(jScrollPane1, BorderLayout.CENTER); } private void initTree() { FileObject folder = Repository.getDefault(). getDefaultFileSystem().findResource(ROOT_NODE); if(folder != null) { /* folder found */ manager.setRootContext(new ExplorerFolderNode(folder)); } } private void initActions() { CutAction cut = SystemAction.get(CutAction.class); getActionMap().put(cut.getActionMapKey(), ExplorerUtils.actionCut(manager)); CopyAction copy = SystemAction.get(CopyAction.class); getActionMap().put(copy.getActionMapKey(), ExplorerUtils.actionCopy(manager)); PasteAction paste = SystemAction.get(PasteAction.class); getActionMap().put(paste.getActionMapKey(), ExplorerUtils.actionPaste(manager)); DeleteAction delete = SystemAction.get(DeleteAction.class); getActionMap().put(delete.getActionMapKey(), ExplorerUtils.actionDelete(manager, true)); } public ExplorerManager getExplorerManager() { return manager; } protected void componentActivated() { ExplorerUtils.activateActions(manager, true); }
protected void componentDeactivated() { ExplorerUtils.activateActions(manager, false); } }
Our next step connects the standard cut, copy, paste, and delete actions to the ExplorerManager
actions. We do this with the initActions()
method. Standard actions are made available to you by the NetBeans Platform. The ExplorerManager
actions give us the ExplorerUtils
class, which we register with our TopComponent
via the ActionMap
key in the ActionMap
. To allow the currently selected node to be available to the view via the TopComponent
Lookup, create a ProxyLookup
via the call to ExplorerUtils.createLookup()
. That provides the currently selected node via the Lookup. The ProxyLookup
is defined as the local Lookup of our TopComponent
via the associateLookup()
method.
To ensure that the actions in the ActionMap
are active, add the ActionMap
to the Lookup (see Chapter 4). Pass the ActionMap
directly into the Lookup via the createLookup()
method, ensuring that the ActionMap
is available via the global Lookup.
To save resources, we add and remove the listeners of the ExplorerManager
's actions in the componentActivated()
and componentDeactivated()
methods, which are called when the window is activated and deactivated.
At this point, you are referred to the many tutorials on http://platform.netbeans.org
, which are easy to understand. Especially in the context of nodes, explorer views, and property sheets, this site contains many code snippets of interest.
In this chapter, you learned about four of the most important NetBeans APIs, together with their dependencies. You learned about these via an example that displays MP3 files. Of the four, the File Systems API is found on the lowest level, as a generic abstraction layer over any kind of data. On top of that, the Data Systems API handles the logic relating to the data abstracted by the File Systems API. For example, you can use the Data Systems API to connect an MP3 file with the functionality that plays it.
The Nodes API, which is above the Data Systems API, is responsible for providing a data presentation layer, as well as for letting the user set properties and invoke actions on the underlying data. Finally, the Explorer & Property Sheet API offers a wide range of Swing containers that display the nodes and their properties to the user.
3.17.76.218