Chapter 7. File Access and Display: Let's Use the NetBeans Platform to Work with Files!

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.

Overview

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.

The data representation architecture of the NetBeans Platform

Figure 7.1. The data representation architecture of the NetBeans Platform

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.

File Systems API

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.

Hierarchy of the FileSystem classes

Figure 7.2. Hierarchy of the FileSystem classes

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.

Operations

Following is an outline of operations provided by the FileObject class.

Obtaining

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.

Creating

The following allows you to create new files or folders using FileObjects:

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");

Renaming

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

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.

Moving

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 FileObjects 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 Files

Reading and writing FileObjects 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().

Monitoring Changes

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.

Data 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 DataObjects and data types used in your application.

Base classes for the development of DataObjects

Figure 7.3. Base classes for the development of DataObjects

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

Base classes for the development of DataObjects

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.

Creating a new file type for MP3 files using the wizard

Figure 7.4. Creating a new file type for MP3 files using the wizard

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.

DataObject

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 FileObjects, 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, DataObjects 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 DataObjects 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 DataObjects.

Looking through the API documentation is very helpful when trying to understand these classes.

Implementing and Adding Cookies

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

Using Cookies

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

Using Cookies

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

Using Cookies

Providing Cookies Dynamically

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 };
   }
}

Creating a DataObject Manually

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.

DataObject Factory

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>

DataLoader

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.

Implementation

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. DataLoaders 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 DataObjects: 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 DataLoaders.

Using a DataObject with only one primary file, as in our MP3 example, select a DataLoader of the type UniFileLoader. Whenever DataLoaders 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 DataObjects 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 FileObjects 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";
   }
}

Registration

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

Nodes API

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.

Hierarchy of Node subclasses

Figure 7.5. Hierarchy of Node subclasses

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

Node Container

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.

Hierarchy of the different Children container classes

Figure 7.6. Hierarchy of the different Children container classes

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

Children.Array

Superclass for all other Children classes. You should not derive from this class directly. This container class manages its nodes in an Array. The nodes will be appended at the end of the array and will be delivered in the same order.

Children.Keys<T>

Typical superclass for your implementation. Nodes are connected with a key. These keys are also used for ordering.

Children.Map<T>

The nodes are stored in a Map. The nodes are associated with a key, which is also used for deleting nodes.

Children.SortedArray

Extends the Children.Array class with a Comparator.

Children.SortedMap<T>

Extends the Children.Map<T> class with a Comparator. Therefore, this class is very similar to Children.SortedArray.

Actions

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 DataObjects, 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.

Event Handling

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

childrenAdded(NodeMemberEvent evt)

Called when child nodes are added

childrenRemoved(NodeMemberEvent evt)

Called when child nodes are removed

childrenReordered(NodeMemberEvent evt)

Called when child nodes are reordered

nodeDestroyed(NodeEvent evt)

Called when the parent node is destroyed

propertyChange(PropertyChangeEvent evt)

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.

Implementing Nodes and Children

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.

Example of the usage of the nodes and explorer views

Figure 7.7. Example of the usage of the nodes and explorer views

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.

Explorer & Property Sheet API

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.

Class hierarchy of different explorer views

Figure 7.8. Class hierarchy of different explorer views

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 TopComponents, 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.

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.75.59