Free 10-week NetBeans Platform Online Course by Sang Shin, Tim Boudreau, and Geertjan Wielenga

10-week Free NetBeans Platform Programming (with Passion!) Online Course

 

Sang Shin, sang.shin@sun.com, Sun Microsystems, www.javapassion.com

 Tim Boudreau, tim.boudreau@sun.com, Sun Microsystems, weblogs.java.net/blog/timboudreau/

 Geertjan Wielenga, geertjan.wielenga@sun.com, Sun Microsystems, blogs.sun.com/geertjan

From August 17th, 2008:

This course is INCOMPLETE. It is a draft ONLY. Starting Date to Be Announced. This course is INCOMPLETE. It is a draft ONLY. Starting Date to Be Announced.




     


Message to potential attendees to this course from Sang Shin, Tim Boudreau, and Geertjan Wielenga


The free online NetBeans Platform course is primarily for developers who are new to the NetBeans Platform. However, even experienced NetBeans Platform developers stand to gain a deeper understanding of the NetBeans Platform by taking this course. Over a period of 10 weeks, you will be guided through the creation of an advanced NetBeans plugin that will integrate basic support for a new language. You will also write a project type for creating projects specific to that language. Even if you are not planning to do those exact things in your own projects, the course will familiarize you with a lot of NetBeans concepts that will be useful in writing any plugin or any application on top of the NetBeans Platform.

At the end of the course, you will have built support for POV-Ray: the Persistence Of Vision Raytracer. POV-Ray is an open-source 3D rendering engine: one that only a programmer could love. Rather than having a graphical modelling tool, it is based around a scene language which is "compiled" into an image.

We will walk through creating several NetBeans modules. One implements support for POV-Ray Scene Files. Another provides a POV-Ray project type, and allows scene files to be rendered into image files. We will also create a module that will provide POV-Ray samples for our users:

Right at the end, you will be able to open POV-Ray files and render them in the IDE. A few tweaks later, you'll have a separate rich-client application, independent of the IDE:

How to Register for the Course

We are going to use a newly created Google alias (netbeanplatform@googlegroups.com) as of August 2008.  Please register yourself by sending a blank email to the following.  (Please read registration FAQ before you sign up.)

Basically, when you send a blank email to the above, you are subscribing our class forum. This forum can be used for posting questions/answers either by email or through the website.  Please use this forum for all class related communication (technical or non-technical).  It is strongly recommended you don't send an email directly to me. For further questions, please see course FAQ below.  Please see the FAQ before posting questions to the class alias or sending an email directly to me.

Topics and Schedule


The general principles of participating in this course are outlined below:

  • Required Knowledge. You are assumed to be familiar with Java and Swing. For example, you will not be told what a "thread" is. You will also not be told what the "event dispatching thread" is. In general, if you have not developed Java applications before, and if you are not comfortable developing Swing applications, then this course is going to be more difficult than it needs to be. To get up to speed, follow the learning trails in the Java Tutorials.

    Once you are up to speed, i.e., you have a basic background in Java and Swing, and you're interested in creating large, complex applications on top of a reliable framework, there should be nothing to stop you!

  • Outline of Lessons. Each lesson will begin with an overview, then provide some Resources, and then continue with the course material.

  • Assignment, Certification, Homework. Each lesson will end with a set of Homework tasks. These will extend on the basic principles covered for that week: in most cases there will be a direct connection to them.

    In order to receive a certificate for completing this course, you will need to successfully complete a 'Student Assignment', as described in detail at the end of this course. If you complete the Student Assignment, you will receive a certificate stating that you are a NetBeans Certified Engineer or a NetBeans Certified Contributor. One of these qualifications will follow automatically from completing this course in its entirety.


Week 1: File Support

In the first week, once we have completed the Resources section, we will create a "module suite" project to contain all the modules we will create throughout this course. Next, also during this week, we will create basic support that enables NetBeans to recognize ".pov" files and to open them in the editor: which will give us a place to hang Actions, provide special icons, and enable other special support specific to ".pov" files.

Resources

Before continuing with the lesson, read through and understand (and, where applicable, take the steps described) in the following documents:

You are assumed to have become very familiar with all of the above documents before continuing.

Video

Watch this video, which will give you a basis for understanding the NetBeans Platform:

Next, read/follow these essential documents:

Lab

1.1 Creating the Module Suite and Projects

Since support for POV-Ray will be made up of multiple modules, we will start by creating a module suite: a container that can hold multiple interdependent modules.

  1. Select File > New Project from the main menu. In the New Project dialog, select NetBeans Modules > Module Suite:

  2. Click Next or press Enter. Name the suite "povsuite" on the next screen and choose a location on your hard disk to put your project:

  3. Press Enter or click Finish. The IDE creates a logical view for your new module suite project:

  4. Now we will create the first module for our suite: support for .pov files: POV-Ray scene language files. Select File > New Project again from the main menu. (Or right-click the Modules node in the Projects window and choose "Add New...") In the New Project wizard, this time you'll choose NetBeans Modules > Module:

  5. Click Next or press Enter. Name the module "povfile" on the next screen, choose a location on your hard disk to put it, make sure that 'Add to Module Suite' is selected, with the previously created 'povsuite' shown:

  6. Press Enter or click Next. On the next page of the wizard, give the module the code name base "org.netbeans.examples.modules.povfile" and the display name "Povray File Support", while making sure that the Generate XML Layer checkbox is selected:

  7. Click Finish or press Enter to create the project. It should automatically be added to the module suite we just created:

  8. We will now repeat the last few steps to create another project, in which we will implement a special POV-Ray project type, whose compile actions will generate an image from a scene file.
    • Select File > New Project again from the main menu.
    • Again, select NetBeans Modules > Module in the New Project wizard, and click Next or press Enter.
    • On the next pages of the wizard, name the module "povproject", give the module the code name base "org.netbeans.examples.modules.povproject" and the display name "Povray Projects".
    • As before, select the "Generate XML Layer" checkbox, so that a layer file will be generated.
    • Click Finish or press Enter to create the project. It should automatically be added to the module suite we just created.

    You should see the following:

1.2 Creating a DataObject: Basic Support for .pov Files

The first thing we will want to create is basic support that enables NetBeans to recognize .pov files and open them in the editor: which will give us a place to hang Actions, provide special icons, and other special support specific to .pov files.

A DataLoader is a factory which is registered against a particular MIME type or file extension. It creates DataObjects: typically one per file. So, when you expand a folder in the Projects window or Files window in the IDE, what happens under the hood is that all of the files in the folder you expanded are found, and for each file, the system locates the DataLoader for its file type and asks it to create a DataObject for it. DataObjects contain logic that enables them to actually parse or understand a files contents, or know what to do with that particular type of file. So there is a 1:1 mapping between file types and DataLoaders.

NetBeans 5.0 and up has a template called "File Type" that makes it very easy to generate basic support for a new file type:

  1. Expand the Povray File Support module and its Source Packages subnode. Right-click the package org.netbeans.examples.modules.povfile and choose New > Other from the popup menu. Select Module Development > File Type in the New File wizard:

    Click Next or press Enter.

  2. On the next screen, you are asked to supply a MIME type and a file extension. Enter "text/x-povray" for the MIME type, and two file extensions, ".pov, .inc" for the file extensions:

    Click Next or press Enter.

  3. On the next screen, you are asked to supply a prefix for the names of several Java classes that will be generated. Enter "Povray". This screen also requests an icon. Any 16x16 gif or png will do, or you can use this one . When you have entered the icon and the name, you should see this:

    Press Enter or click Finish and the IDE will generate the Java classes and metadata files needed for basic POV-Ray file support in NetBeans.

    You should now have the following file structure in the povfile package:

    org.netbeans.examples.modules.povfile/

    • Bundle.properties: A resource bundle for miscellaneous localized strings.
    • layer.xml: A module layer file which allows the module to install some objects declaratively.
    • povicon.gif: The icon you chose in the wizard, which will appear whenever Povray files are edited or displayed in an explorer view.
    • PovrayDataObject.java: This is the object that understands what a .pov file is. If we were to provide advanced support for POV-Ray files, we would probably parse those files here, and provide some sort of model of the structure of the file that could be shown in Navigator or manipulated programmatically.
    • PovrayResolver.xml: This is a small bit of XML that declares that .pov and .inc files map to the MIME type text/x-povray (which we have invented for purposes of this tutorial). This XML file is referenced from the module's layer.xml file.
    • PovrayTemplate.pov: This is an empty template POV-Ray file which can be modified and will be used as the basis of new POV-Ray files in the New File wizard.

  4. At this point we have basic support for POV-Ray files: if you right-click the module suite and click Run, NetBeans will run with both of the modules installed: and if you create a fake .pov file in your home directory and then browse, for example, in the Favorites window (Window > Favorites from the main menu), you will see that it is indeed recognized by NetBeans, and has the icon that you specified:

  5. There are a few idioms worth noticing in the generated code:

    • The DataLoader is registered in the layer file: if you expand Povray File Support > Important Files and open the XML layer in the editor, you will see the following:

      <folder name="Factories">
          <file name="PovrayDataLoader.instance">
              <attr name="SystemFileSystem.icon" urlvalue="nbresloc:/org/netbeans/examples/modules/povfile/povicon.gif"/>
              <attr name="dataObjectClass" stringvalue="org.netbeans.examples.modules.povfile.PovrayDataObject"/>
              <attr name="instanceCreate" methodvalue="org.openide.loaders.DataLoaderPool.factory"/>
              <attr name="mimeType" stringvalue="text/x-povray"/>
          </file>
      </folder>

    • The layer.xml file now also contains a folder, defined in XML, Loaders/text/x-povray/Actions, and all of the actions that appear on the popup menu of a .pov or .inc file are registered there. Currently the list only contains standard NetBeans API actions such as Cut and Delete (as you can see in the screenshot above); we will eventually add more.

Homework

Things to think about and/or activities to complete:

  • In this lesson, you have let the NetBeans Platform recognize a file based on its file extension. However, what about XML files? Most XML files have an ".xml" file extension. Read the lesson above again, very carefully, and try to figure out how the NetBeans Platform is able to distinguish between different kinds of XML files. Then create a new plugin that will let the NetBeans Platform distinguish one specific kind of XML file from another! (Afterwards, don't lose this plugin, because you will be extending it for homework in Week 8.)

  • Also in this lesson, you learned how to use a "Module Suite" project type. In the "Resources" section, you learned how to use a "NetBeans Platform Application" project type. Do you know what the difference is between these two project types? Don't continue with this course until you are sure you know the difference! (To find out, create both types of project and then find out what the difference is between them. Then ask yourself when you would use the one and when you would use the other! Why do you think you were instructed to use "Module Suite" project type, instead of the "NetBeans Platform Application" project type, in this course?)

  • Next, think about the words "plugin" and "module". In the world of the NetBeans Platform, these two words mean almost the same thing. However, there is a difference. Can you think what it is? How does the word "module" relate to the word "plugin"?

  • Finally, look at the JUnit test generated by the "File Type" wizard. Think about what it does. (Hint: look in the layer.xml file.) Try to, from now onwards, for homework at the end of each week, create a JUnit test for the code created during the week. To help you, read the "Writing tests" section of the NetBeans Developer FAQ.

Next Week

Next week we will cover designing and planning our project type and file support and how they will interrelate.


return to the topics



Week 2: Project Type Design

In this part of the course, we will walk through how to create a basic project type. It will be a project type that supports building 3D graphics scenes using POV-Ray's scene language, and eventually, rendering them as images and displaying the result in NetBeans.

It is assumed you have completed the steps in the previous session for creating basic POV-Ray support.

This session will go through the up-front design and the thinking through needed to successfully implement a project type that will serve its users well. The subsequent tutorial walks through the implementation of the basic project.

Resources

This week, there will be no coding that you will need to do during the lab. You will be focusing on questions of design and thinking about how best to implement the project we're working on in this course.

Nevertheless, the resources for this week will let you get your hands dirty creating some complete NetBeans Platform applications, to give you a feel for what that entails. Do these tutorials slowly and carefully and try to understand everything you do. Think about the design of the application in question as you work through the tutorials. Therefore, before continuing with the lesson below, do the following tutorials:

Lab

2.1 Design and Coupling

A NetBeans project is a directory on disk; typically there is some signature, such as having a subdirectory with a specific name, which identifies that directory as being a project.

The major requirement of identifying a folder as being a project is that the test fail quickly: the code that determines that a folder is not a project must complete very quickly, because it is going to be called once for every folder visible in the file chooser that lets users open projects. We will stick with what works, and use a specific subdirectory name to identify our POV-Ray projects.

Notice that we implemented support for .pov and .inc files in a separate module. Design-wise this makes sense: data recognition and project types are orthagonal: and it provides a good demonstration of how to do loose coupling between modules.

So at this point we need to think about what we're designing, what functionality belongs to which module, and what will serve our users' needs best. Before we do any coding, some design has to happen. Here's what we know about POV-Ray and its usage patterns:

  1. Item 1: POV-Ray has a scene language, and it "compiles" textual .pov files down into image files.

  2. Item 2: A .pov file can reference other files, which typically get the extension .inc.

  3. Item 3: POV-Ray has a huge number of options such as antialiasing, jitter, output image size, quality, number of reflections, animation clock.

  4. Item 4: Users will render small test versions of a scene at low quality to check their work as they go; large, high quality renders can take a long time.

  5. Item 5: Typically the user knows the size and aspect ratio they'll want for the final output, but that's also not what most renders will use.

  6. Item 6: To save rendering time while working, a user may split individual objects from a scene into separate files and render them individually as they work.

2.2 Design Choices: Don't Try to Save the World

The biggest killer of projects is trying to "save the world": trying to provide every combination of every possible option to the user, such that the user is confronted with a bewildering array of choices when most of the time they want to do something very simple. Trying to support every imaginable permutation of anything leads to projects which are permanently six months from completion. So we need to limit the scope while providing value to the user. But we also want to keep power users happy and give them the ability to do what they need to do. So we have some choices to make, and some hard thinking to do about what is the right UI to balance the power of POV-Ray with the need to keep things simple and easy to use for beginners.

The first question, from item 1, is, which module owns the rendering infrastructure? Well, when you render an image, you need a place to put it. A project owns a directory and its subdirectories. So we'll make an executive decision right now that we don't render a random .pov file on disk: it must belong to a project which can provide a place to put the result. So the code that actually calls out to POV-Ray will be part of the project support module, not the file support module.

A follow-on to this is, should it be possible to render absolutely any .pov or .inc file? Probably yes, due to item 6: but we'll only do that if it belongs to a project.

Do we want to provide structure (subfolders, etc.) in POV-Ray projects? It would probably be nice for the future, but to keep the scope of things manageable, we're not going to do that now. In practice, POV-Ray projects can be just as messy as projects written in C, with source files scattered hither and yon. We are not going to try to solve all the world's problems here; but neither are we going to impose our own idea of what a well structured POV-Ray project looks like on the user.

How many of the myriad command-line options that POV-Ray supports should we expose the user to? The real usage pattern here is that, for test renders, any of a few sets of standard settings will do for almost all cases. So we will provide a set of standard resolutions like 320x200, 640x480, 1024x768. Those settings will be unmodifiable. But, associated with each project will be a set of production settings, which are saved with the project, are shareable and go with the project wherever it goes. We will provide a basic GUI customizer for the production settings. It will not cover every possible setting POV-Ray has: however, the customizer will be writing into the project's project.properties file: and a power user can edit in their own settings to do anything they want, so the full power of POV-Ray stays available to power users: any desired line switch can be encoded into the project's Properties file and end up passed on the command line to POV-Ray.

Because of item 6, we know there can be multiple files in a project, but only one is the master scene that is the final output of the project. So we will give our projects the concept of a main file which is what gets rendered when they invoke Build, and we'll need a UI to select which file it is.

2.3 Physical Structure of a POV-Ray Project

We also need to make some decisions about what will be stored on disk and where. We know that projects typically have Build and Clean actions, and these will be useful for POV-Ray projects as well. Where should scene files go? Well, we want to keep some flexibility for future versions, so we don't want them in the root directory of our projects: we'll create a scenes/ folder for them.

We also know we will end up with image files from rendering. The Clean operation will be much less complex to implement if it just means deleting a directory. And the UI will be less cluttered if we put images in their own directory, so rendered images will go in an images/ subdirectory of the project. The project UI won't even show the images/ directory: you'll just right click a scene file and select View, to render and show a single file, or build the project to show the "main file". As with compiling, we will do a date check to see if the source file is newer than the image file, and if so, render it again.

We also know we need to use a subdirectory to identify our project type, and to store configuration data that should be saved, such as which file is the main file and what are the production renderer settings. So we'll decide that every POV-Ray project will have a pvproject subdirectory, and in that directory will be a properties file. So on disk, a typical POV-Ray project will look like:

  • Wonderland/

    • images/
      • Wonderland.png
    • pvproject/
      • project.properties
    • scenes/
      • Alice.inc
      • Wonderland.pov
      • LookingGlass.inc
      • MadQueen.inc
      • MockTurtle.inc

2.4 Coupling Between POV-Ray Files and POV-Ray Project

Typically a user is going to be dealing with files, and we already decided that a user should be able to render any file in a project; and we know we are going to have to give the user the ability to pick which file is the main file for a project. That means our Nodes for .pov files will need to have Actions that will be invoking rendering and main-file-setting code that belongs to the project module, not to their module. So we know we that the project module will need to expose some API for doing these things, and the Node of a POV-Ray file will need to be able to find the project that owns it and call this code. But if a .pov is orphaned on disk, or is inside, say, a J2EE project for some reason, it must fail gracefully.

This gives a great opportunity to demonstrate how loose coupling between modules is done. There are two simple mechanisms that will allow us to easily do this: The first is FileOwnerQuery: this is a class from the Project module, which allows one to find out what project owns any file. The second is that Project, the interface we implement to create our project type, has a method getLookup(). So we will provide some interfaces in the POV-Ray project type which we'll make available as API. When a node finds out what project its file belongs to, it can simply request the implementation of one of those interfaces. If it gets non-null, it can call the rendering or main-file-setting functionality; if it gets null, or the project is null, those actions will just be disabled.

Homework

Start thinking about an application that you will create on the NetBeans Platform! Or start thinking about a plugin that you will create. Read through the thoughts above again and then try to apply them to your own project. Try and plan your project and divide it into different stages: what will the first release consist of? Write down some requirements for your project and prioritize them. Then think about why something is prioritized higher than something else! Whose criteria did you use?

Next Week

Design is an inescapable first-step in developing modules. It pays to think hard about what functionality belongs where, and what the user experience should be before starting to code.

Next week we will cover providing project support for POV-Ray.


return to the topics



Week 3: Implementing a Project Type

In this section, you create a new project type for POV-Ray projects.

The NetBeans APIs enable you to extend existing project types, either by removing or adding logical view nodes, lookup items, and customizer panels from projects that already exist. If you are reading this section in order to learn about creating a project type for your own scenario, you may want to consider taking the other route (i.e., extend an existing project type rather than create a new one from scratch), especially if the project type you want to create is similar to one of the existing types. For information on this approach, see the NetBeans Project Type Extension Module Tutorial.

Resources

Before continuing with the lesson, read through and understand (and, where applicable, take the steps described) in the following documents:

Lab

3.1 Setting Up Dependencies

There are a few APIs which we will need to use classes from: so before starting to code, let's add dependencies from the Povray Projects module to those:

  1. Right-click the Libraries node and then choose "Add Module Dependency...", as shown below:

  2. In the dialog that appears, type "ProjectFactory". The Project API module should become selected in the list below: it is the module that provides this class, so we need a dependency on it to be able to use the class.

  3. Press Enter or click OK.

  4. Click the Add Dependency button again. In the add dependency dialog, type "FileObject". When "File System API" becomes selected, press Enter or click OK.

  5. Repeat these steps again, typing the name "Lookup", and adding a dependency on the Utilities API:

  6. Repeat these steps yet again, typing the name "AbstractNode", and adding a dependency on the Nodes API:

  7. Repeat these steps once again, typing the name "DataFolder", and adding a dependency on the Datasystems API:

  8. Repeat these steps once more, typing the name "LogicalViewProvider", and adding a dependency on the Project UI API:

  9. Make sure that you now have the following list of NetBeans libraries available to your project:

    Press Enter or click OK to dismiss the Project Properties dialog.

3.2 Creating the Project Factory

As with DataObjects and DataLoaders, the system keeps a registry of things that can identify a directory as being a project and create a Project object to represent it. So the first step in creating a our own project type is to create a factory: an implementation of ProjectFactory from the Project API: which can figure out if a directory is a POV-Ray project and, if it is one, make an instance of our Project implementation for it.

  1. Right-click the org.netbeans.examples.modules.povproject package, and choose New > Java Class. In the New File wizard that appears, enter the name "PovrayProjectFactory":

    Press Enter or click Finish to create the new file.

  2. In the code editor, modify the signature line of PovrayProjectFactory as follows:

    public class PovrayProjectFactory implements ProjectFactory {

    Press Ctrl-Shift-I to Fix Imports.

  3. Position the caret somewhere in the class signature line. When the lightbulb glyph appears in the margin, press Alt-Enter, and then Enter again to accept the hint "Implement All Abstract Methods".

    You should now see the following class definition:

    public class PovrayProjectFactory implements ProjectFactory {
    
        public boolean isProject(FileObject arg0) {
            throw new UnsupportedOperationException("Not supported yet.");
        }
    
        public Project loadProject(FileObject arg0, ProjectState arg1) throws IOException {
            throw new UnsupportedOperationException("Not supported yet.");
        }
    
        public void saveProject(Project arg0) throws IOException, ClassCastException {
            throw new UnsupportedOperationException("Not supported yet.");
        }
    
    }

  4. Add the following constants to the head of the PovrayProjectFactory class definition:

    Type Psfs and press the TAB key to quickly create 'public static final String' declarations.

    public class PovrayProjectFactory implements ProjectFactory {
            
        public static final String PROJECT_DIR = "pvproject";
        public static final String PROJECT_PROPFILE = "project.properties";

  5. The first method we will implement is the isProject() method. This method needs to be very fast: it should determine whether or not a directory is a project as quickly as possible, because it will be called once for each directory shown in the file chooser when the user selects File > Open Project.

    Implement the method as follows, first replacing the method parameter's name from "arg0" to "projectDirectory", and then filling out the body of the method:

       @Override
       public boolean isProject(FileObject projectDirectory) {
            return projectDirectory.getFileObject(PROJECT_DIR) != null;
        }

    This simple test for the presence of a subdirectory called "pvproject" is all we need to determine that something is not one of our projects.

    Next, we will implement the code that actually loads a project, given a directory. The project system handles caching of projects, so all that's needed here is to create a new project. As before, make sure that the names of the method parameters match the names of the parameters in the body of the method:

        @Override
        public Project loadProject(FileObject dir, ProjectState state) throws IOException {
            return isProject (dir) ? new PovrayProject (dir, state) : null;
        }

    The only interesting thing here is the ProjectState object, which we pass along with the directory to our project's constructor. It is provided to us by the project system, and can be used to mark a project as needing to be saved (there is no "Save Project" action because the system will invoke saveProject() for us behind the scenes). We will use it later to do that when the user changes the main file of the project, which will be written to disk in the project.properties when our project is closed.

    The reference to a class called "PovrayProject" is now underlined in red, because it does not yet exist. We will create it in a later step. Until then, don't worry about the red underlined class reference.

  6. The final thing to implement is saveProject(): this is what will write out any unsaved changes to disk when a POV-Ray project is closed, or when NetBeans shuts down:

    @Override
    public void saveProject(final Project project) throws IOException, ClassCastException {
    
        FileObject projectRoot = project.getProjectDirectory();
        if (projectRoot.getFileObject(PROJECT_DIR) == null) {
            throw new IOException ("Project dir " + projectRoot.getPath() + " deleted," +
                    " cannot save project");
        }
    
        //Force creation of the scenes/ dir if it was deleted
        ((PovrayProject) project).getScenesFolder(true);
    
        //Find the Properties file pvproject/project.properties,
        //creating it if necessary
        String propsPath = PROJECT_DIR + "/" + PROJECT_PROPFILE;
        FileObject propertiesFile = projectRoot.getFileObject(propsPath);
        if (propertiesFile == null) {
            //Recreate the Properties file if needed
            propertiesFile = projectRoot.createData(propsPath);
        }
    
        Properties properties = (Properties) project.getLookup().lookup (Properties.class);
    
        File f = FileUtil.toFile(propertiesFile);
        properties.store(new FileOutputStream(f), "NetBeans Povray Project Properties");
        
    }

    Press Ctrl-Shift-I to Fix Imports.

    We haven't written the PovrayProject yet, but from this code it's pretty clear what it will look like: We are creating the scenes/ directory if it does not exist or was deleted; we fetch a Properties object out of the project's Lookup, and save it into pvproject/project.properties: that's all there is or will be to saving a POV-Ray project.

3.3 Registering the Project Factory

The system needs to know about our project type, for this module to do anything. We will register our project type into the default lookup using the technique of adding a file to META-INF/services in our module's jar:

  1. Expand the Important Files node and then expand the META-INF services node:

  2. In the "<all services>" node, find "org.netbeans.spi.project.ProjectFactory" and right-click it, choosing Add New Service:

  3. Browse to org.netbeans.examples.modules.povproject.PovrayProjectFactory:

    Press OK and then press OK again.

  4. Notice that your project now contains a new folder structure containining a single file:

    The file contains a single line, consisting of the fully qualified name of the class that implements the class after which the file is named.

That's all it takes to register our project type, so that when our module is loaded, NetBeans will start recognizing POV-Ray projects.

3.4 Implementing PovrayProject

Now we need to create the Java class that represents a POV-Ray project: this is what our PovrayProjectFactory will create if the user opens a project that it owns. The Project API in NetBeans is quite simple. A "project", programmatically, is the association of a directory on disk with a Lookup: a bag-of-stuff that can be queried for known interfaces or abstract Java classes. The Project API then defines some interfaces and classes that should be available from a Project's Lookup.

So the first thing will be to create our implementation of org.netbeans.api.project.Project.

  1. Right-click the org.netbeans.examples.modules.povproject package in the Povray Projects project, and choose New > Java Class. In the New File wizard that appears, enter the name "PovrayProject":

    Press Enter or click Finish to create the new file.

  2. In the code editor, modify the signature line of PovrayProject as follows:

    public final class PovrayProject implements Project {

    Press Ctrl-Shift-I to Fix Imports.

  3. Position the caret somewhere in the class signature line. When the lightbulb glyph appears in the margin, press Alt-Enter, and then Enter again to accept the hint "Implement All Abstract Methods".

    Implement the top of the file as follows:

    public final class PovrayProject implements Project {
    
        public static final String SCENES_DIR = "scenes"; //NOI18N
        public static final String IMAGES_DIR = "images"; //NOI18N
    
        private final FileObject projectDir;
        LogicalViewProvider logicalView = new PovrayLogicalView(this);
        private final ProjectState state;
    
        public PovrayProject(FileObject projectDir, ProjectState state) {
            this.projectDir = projectDir;
            this.state = state;
        }
    
        public FileObject getProjectDirectory() {
            return projectDir;
        }
    
        FileObject getScenesFolder(boolean create) {
            FileObject result =
                projectDir.getFileObject(SCENES_DIR);
    
            if (result == null && create) {
                try {
                    result = projectDir.createFolder(SCENES_DIR);
                } catch (IOException ioe) {
                    Exceptions.printStackTrace(ioe);
                }
            }
            return result;
        }
    
        FileObject getImagesFolder(boolean create) {
            FileObject result =
                projectDir.getFileObject(IMAGES_DIR);
            if (result == null && create) {
                try {
                    result = projectDir.createFolder (IMAGES_DIR);
                } catch (IOException ioe) {
                    Exceptions.printStackTrace(ioe);
                }
            }
            return result;
        }
    
        public Lookup getLookup() {
            throw new UnsupportedOperationException("Not supported yet.");
        }
    
    }

    Press Ctrl-Shift-I to Fix Imports.

    The getScenesFolder method and the getImagesFolder method we will use later on: they define (and can create) the scenes code and images folders that POV-Ray source files and their resulting image files will go into when the project is rendered.

  4. The actually interesting code goes into our implementation of getLookup(). Eventually we will put some of our own interfaces into the project's Lookup; for now it will be mainly standard stuff: interfaces provided by the Project API module which we will implement. Implement getLookup() as follows:

        private Lookup lkp;
        
        public Lookup getLookup() {
            if (lkp == null) {
                lkp = Lookups.fixed(new Object[] {
                    this,  //project spec requires a project be in its own lookup
                    state, //allow outside code to mark the project as needing saving
                    new ActionProviderImpl(), //Provides standard actions like Build and Clean
                    loadProperties(), //The project properties
                    new Info(), //Project information implementation
                    logicalView, //Logical view of project implementation
                });
            }    
            return lkp;
        }
    

    Press Ctrl-Shift-I to Fix Imports. Three lines will still be underlined in red. In the coming steps, we will create the code that those lines reference.

    The one interesting thing in the code above is the call to loadProperties(): for storing project settings, we will use a Properties file. So all we do here is read it into a Properties object, and make that object available through the project's Lookup.

  5. We will want to save any changes made to this properties object, so we'll use a bit of cleverness and create a Properties subclass that will mark the project as needing saving whenever something calls put(). Add the following to the PovrayProject class and fix imports:

    private Properties loadProperties() {
        FileObject fob = projectDir.getFileObject(PovrayProjectFactory.PROJECT_DIR +
            "/" + PovrayProjectFactory.PROJECT_PROPFILE);
        Properties properties = new NotifyProperties(state);
        if (fob != null) {
            try {
                properties.load(fob.getInputStream());
            } catch (Exception e) {
                Exceptions.printStackTrace(e);
            }
        }
        return properties;
    }
    
    private static class NotifyProperties extends Properties {
        private final ProjectState state;
        NotifyProperties (ProjectState state) {
            this.state = state;
        }
    
        @Override
        public Object put(Object key, Object val) {
            Object result = super.put (key, val);
            if (((result == null) != (val == null)) || (result != null &&
                val != null && !val.equals(result))) {
                state.markModified();
            }
            return result;
        }
    }

    Other than that, the things in the Lookup are what should typically be found in the Lookup of any project: the project itself (the project infrastructure reserves the right to wrap any Project type in a wrapper Project object, so this guarantees being able to get at the real project instance), its state, an ActionProvider to handle standard commands like Build and Clean, a ProjectInformation implementation that supplies the display name and icon for the project. The last thing in the lookup is the logical view which we will come to next: this is what provides a Node for the project that will be displayed on the Projects window in NetBeans IDE.

  6. There are two remaining classes we need to create: the implementations of ActionProvider and ProjectInformation. We will simply stub these for now: add these two classes as inner classes of PovrayProject:

        private final class ActionProviderImpl implements ActionProvider {
            public String[] getSupportedActions() {
                return new String[0];
            }
    
            public void invokeAction(String action, Lookup lookup) throws IllegalArgumentException {
                //do nothing
            }
    
            public boolean isActionEnabled(String action, Lookup lookup) throws IllegalArgumentException {
                return false;
            }
        }
    
        /** Implementation of project system's ProjectInformation class */
        private final class Info implements ProjectInformation {
            public Icon getIcon() {
                return new ImageIcon (ImageUtilities.loadImage(
                        "org/netbeans/examples/modules/povproject/resources/povicon.gif"));
            }
    
            public String getName() {
                return getProjectDirectory().getName();
            }
    
            public String getDisplayName() {
                return getName();
            }
    
            public void addPropertyChangeListener (PropertyChangeListener pcl) {
                //do nothing, won't change
            }
    
            public void removePropertyChangeListener (PropertyChangeListener pcl) {
                //do nothing, won't change
            }
    
            public Project getProject() {
                return PovrayProject.this;
            }
        }
        

    When you fix imports, make sure that the Utilities class you use is org.openide.util.Utilities.

3.5 Implementing the Logical View

At this point, only one line in the code you entered above should be marked as being an error:

    LogicalViewProvider logicalView = new PovrayLogicalView(this);

In the IDE, what you see in the Projects window is a "logical view" of your project. This is a view that may not exactly reflect the structure of files on disk (the Files window is for that), but is more convenient to work with: for example, collapsing a tree of directories into a single node with a Java package name.

What we will implement now is a LogicalViewProvider. This is basically a factory that produces a Node that represents the project. What child Nodes that Node has, and what actions are available on them, is up to us.

  1. Right-click the org.netbeans.examples.modules.povproject package in the Povray Projects project, and choose New > Java Class. In the New File wizard that appears, enter the name "PovrayLogicalView":

  2. Press Enter or click Finish to create the new file.

  3. In the editor, modify the signature line of PovrayLogicalView as follows:

    class PovrayLogicalView implements LogicalViewProvider {

    Press Ctrl-Shift-I to Fix Imports.

  4. Position the caret somewhere in the class signature line. When the lightbulb glyph appears in the margin, press Alt-Enter, and then Enter again to accept the hint "Implement All Abstract Methods".

    We now have a skeleton implementation of our logical view.

    Part of the value of having a concept of a project is the ability to present data in a way that is closer to the way a user will think about their project than the structure of files on disk may be. The logical view of a project should present a simplified structure showing users what they need to get their work done.

    In our case, we already decided that the user did not need to see the images/ subdirectory, they should just be able to click a scene file and choose View, and that we want to put scene files in a scenes/ subdirectory. So the logical thing to do for our logical view is to have it show the contents of that scenes/ directory. We can return whatever Node we want as the root of our logical view of the project, and NetBeans makes using the content of the scenes/ subdirectory very easy.

    In the Nodes API is a class called FilterNode. What it does is wrap an existing Node, and by default, simply expose the same child nodes, display name, icon, actions, etc. as the original. We can subclass FilterNode to change its icon and the set of actions available on it. The DataLoader infrastructure already provides a loader that recognizes Filesystem folders: an API class called DataFolder. So we get the original node for the folder for free: we just need to provide a subclass that uses our icon and (eventually) actions.

    We can now implement PovrayLogicalView as follows and then fix imports:

    class PovrayLogicalView implements LogicalViewProvider {
    
        private final PovrayProject project;
    
        public PovrayLogicalView(PovrayProject project) {
            this.project = project;
        }
    
        public org.openide.nodes.Node createLogicalView() {
    
            try {
    
                //Get the scenes directory, creating it if deleted:
                FileObject scenes = project.getScenesFolder(true);
    
                //Get the DataObject that represents it:
                DataFolder scenesDataObject =
                        DataFolder.findFolder (scenes);
    
                //Get its default node: we'll wrap our node around it to change the
                //display name, icon, etc:
                Node realScenesFolderNode = scenesDataObject.getNodeDelegate();
    
                //This FilterNode will be our project node:
                return new ScenesNode (realScenesFolderNode, project);
                
            } catch (DataObjectNotFoundException donfe) {
                Exceptions.printStackTrace(donfe);
                //Fallback: the directory couldn't be created -
                //read-only filesystem or something evil happened:
                return new AbstractNode (Children.LEAF);
            }
    
        }
    
        /** This is the node you actually see in the Projects window for the project */
        private static final class ScenesNode extends FilterNode {
        
            final PovrayProject project;
        
            public ScenesNode (Node node, PovrayProject project) throws DataObjectNotFoundException {
                super (node, new FilterNode.Children (node),
                        //The projects system wants the project in the Node's lookup.
                        //NewAction and friends want the original Node's lookup.
                        //Make a merge of both:
                        new ProxyLookup (new Lookup[] { Lookups.singleton(project),
                        node.getLookup() }));
                this.project = project;
            }
    
            @Override
            public Image getIcon(int type) {
                return ImageUtilities.loadImage (
                    "org/netbeans/examples/modules/povproject/resources/scenes.gif");
            }
    
            @Override
            public Image getOpenedIcon(int type) {
                return getIcon(type);
            }
    
            @Override
            public String getDisplayName() {
                return project.getProjectDirectory().getName();
            }
        }
    
        @Override
        public Node findPath(Node root, Object target) {
            //leave unimplemented for now:
            return null;
        }
    
    }

    When you fix imports, make sure to include org.openide.filesystems.FileObject and org.openide.util.ImageUtilities.

    The interesting code above is in the method createLogicalView(). What we do there is quite simple and elegant: we have already decided that there will be a scenes/ directory in our project, and that's where new .pov and .inc files will be created. And that is all we want to expose to the user when they interact with one of our projects. So, we simply find the Node for that folder in the real filesystem on disk, and wrap it in our own FilterNode, which can expose whatever actions, icon, child Nodes or properties we choose. Essentially, the logical view of the project is a view of a subdirectory of the project, with a special icon and (eventually) set of actions.

    The final method, findPath() allows a user to use a keystroke to select whatever they're editing in the Projects window: we will leave that unimplemented for now.

  5. One final thing we need to do is to provide the icon referenced from PovrayLogicalView.ScenesNode.getIcon() above. Any 16x16 .gif or .png file will do, or you can use this one . Create a new Java package "resources" underneath org.netbeans.examples.modules.povproject, and copy or save the image file there, modifying the file name in the source code if necesary. Similarly, earlier we referred to povicon.gif, for which purpose you can use this one , placing it in the same Java package, that is, org.netbeans.examples.modules.povproject.resources:

Wrapping Up

We now have a working (albeit not terribly useful) implementation of POV-Ray projects. As yet we have no way to create such a project on disk, but if you were to have one, you could open it and view it.

  1. If you want to test your code at this point, create a folder/file structure as follows on disk. All these files can be empty; it's just the structure that matters at this point:

    • Wonderland/

      • images/
        • Wonderland.png
      • pvproject/
        • project.properties
      • scenes/
        • Alice.inc
        • Wonderland.pov
        • LookingGlass.inc
        • MadQueen.inc
        • MockTurtle.inc

  2. The folder structure should now look as follows, on disk:

  3. Now attempt to open it as a project, by running the module suite and, in the new instance of the IDE, using File > Open Project. Once you have done so, the Projects window and Files window should look as follows:


Homework

Choose two (or more!) of the tasks below:

  • You've been introduced to the NetBeans FileUtil class in section 3.2. Read the related Javadoc very carefully, because it is a key class, providing a lot of useful functionality that you will be needing over and over again. Can you use 3 methods from that class within one brand new module? How about 5? 10? How many methods from this class can you squeeze into one single module?

  • Create any kind of folder structure on disk. Make sure it has various kinds of folders and files. Also make sure to make something be unique: for example, maybe one of the folders must be present in every project of this kind, or maybe one of the files needs to have a special name. Then... create a plugin that will let you open that folder structure into NetBeans IDE!

  • Create a brand new project type for JavaHelp help sets! Start by creating a new Module Project and then use the JavaHelp Helpset wizard (in the "Module Development" section of the New File wizard) to create a set of JavaHelp help files. Then look in your file system (outside NetBeans IDE) to understand how such a set of folders/files is constructed. Next, create a new type of project that will enable you to open that set of folders/files as a "JavaHelp Project" into NetBeans IDE!

  • Click all the Javadoc links in this week's lesson. Then write one sentence about each class, in such a way that it makes sense to you. Then create a new entry in your blog and post your summaries there!

  • Create a JUnit test for the code created during this week. Which pieces of code does it make sense to test? To help you, read the "Writing tests" section of the NetBeans Developer FAQ.

Next Week

Next week we will begin to add truly useful functionality to our projects... project templates.


return to the topics



Week 4: Providing Project Templates

We left off with a module that lets us open POV-Ray projects, and basic support for POV-Ray files, but no way to create a new POV-Ray project.

So the first step is to add the ability to use the New Project Wizard to create POV-Ray projects. NetBeans 5.0 and up has the ability to embed a project in a module as a ZIP file, and add the necessary configuration and code to make it available from the New Project wizard and unpack it in a directory of the user's choice. We will make use of that functionality to create our project templates.

Resources

Before continuing with the lesson, read through and understand (and, where applicable, take the steps described) in the following documents:

  • NetBeans Project Sample Module Tutorial: covers a lot of the concepts you will encounter throughout the course.
  • NetBeans Developer FAQ: browse through the whole FAQ and have a look around. The FAQ is the most valuable resource to any NetBeans Platform developer. It contains definitions and descriptions of API classes and concepts, and how to do various things.

Lab

4.1 Project Templates

First we need to have a project to ZIP up, so we will create that by hand. You can do this in the IDE, just by creating the appropriate folders and files.

  1. First create a new module. Do this by right-clicking the Modules node within the module suite, in the Projects window. Choose Add New:

  2. In the New Module Project wizard, name the project povsamples:

    Click Next or press Enter.

  3. In Code Name Base, type org.netbeans.examples.modules.povsamples. In Module Display Name, type Povray Project Samples.

    Click Finish or press Enter. The module suite should now contain three modules:

  4. Switch to the Files window in the IDE and find the root folder for the Povray Project Samples project, which is highlighted below:

  5. Create the following directory structure underneath that directory, that is, underneath org.netbeans.examples.modules.povsamples, in the Files window:

    • templates/ : a root directory for our template projects

      • EmptyPovrayProject/ : Base directory for an empty project

        • images/
        • pvproject/
          • project.properties
        • scenes/
          • EmptyPovrayProject.pov
      • SamplePovrayProject/ : Base directory for a project with sample .pov files

        • images/
        • pvproject/
          • project.properties
        • scenes/
          • SamplePovrayProject.pov
  6. You should now see the following in the Files window:

  7. We should have some content for the sample POV-Ray project's file. You can copy and paste the initial content (a 3D model of the NetBeans logo) from this file into "SamplePovrayProject.pov":

4.2 Adding the Project Templates

Now we are ready to add our sample projects: but here we have to cheat just a little: the IDE will only package up a sample project that it has open, and in our development IDE we don't have support for POV-Ray projects, so our hand-created projects won't be recognized. But, we already have a module that provides support for POV-Ray projects available. So we will cheat just a little bit and use that to fool the IDE into embedding our new POV-Ray projects in our module:

  1. Right click the module suite, and choose Run. When the IDE is started, open templates/EmptyPovrayProject and templates/SamplePovrayProject in that instance of the IDE. Optionally, you can do so from the Favorites window, as shown here:

  2. Now, open the Povray Project Samples module in that IDE instance as well. You should now see the following three projects in the Projects window:

  3. Expand the Povray Project Samples module and right click the package org.netbeans.examples.modules.povproject.povsamples and choose New > Other. In the New File Dialog, select Module Development > Project Template:

    Press Enter or click Next.

  4. On the next page of the wizard, select EmptyPovrayProject from the combo box: this is what we will package up:

    Click Next or press Enter.

  5. Now you are prompted for a name. Enter "EmptyPovrayProject" for the Template Name, and "Empty Povray Project" for the display name. Select Java as the category (we can create our own POV-Ray category later), and type .empty at the end of the Package name, so that the package name will become org.netbeans.examples.modules.povsamples.empty:

    Click Finish or press Enter.

  6. Now repeat the above steps for templates/SamplePovrayProject, calling it Sample Povray Project, and using the package name org.netbeans.examples.modules.povsamples.sample:

  7. You should now see the following in the Projects window:

    Close the copy of NetBeans that has our modules installed: it's done its job for now.

4.3 Optimizing the Project Templates

The above steps created a number of files on disk: there are two new ZIP files in our module that are zipped copies of the projects. The following new files were created. Some of them we can delete, as instructed later below: our two templates can share wizard code for instantiating them from the New Project wizard:

  • povsamples/

    • EmptyPovrayProjectProject.zip
    • SamplePovrayProjectProject.zip
    • empty/
      • EmptyPovrayProjectDescription.html Description that will be shown when the user selects the project in the New Project wizard
      • EmptyPovrayProjectPanelVisual.form Form file for the panel in the wizard that lets the project be named, which we can customize
      • EmptyPovrayProjectPanelVisual.java Corresponding Java file for the template
      • EmptyPovrayProjectWizardIterator.java A "wizard iterator" which provides the additional steps in the wizard after selecting the template
      • EmptyPovrayProjectWizardPanel.java A wrapper object that delays creating EmptyProjectPanelVisual until the user navigates to that page in the wizard
    • sample/
      • SamplePovrayProjectDescription.html Description that will be shown when the user selects the sample project in the New Project wizard
      • SamplePovrayProjectPanelVisual.form can be deleted
      • SamplePovrayProjectPanelVisual.java can be deleted
      • SamplePovrayProjectWizardIterator.java can be deleted
      • SamplePovrayProjectWizardPanel.java can be deleted

The layer.xml file has also been modified, to register these two projects so they are added to the New Project wizard. The following code has been added to the layer.xml:

<folder name="Templates">
    <folder name="Project">
        <folder name="Standard">
            <file name="EmptyPovrayProjectProject.zip" url="EmptyPovrayProjectProject.zip">
                <attr name="SystemFileSystem.localizingBundle" 
                            stringvalue="org.netbeans.examples.modules.povsamples.Bundle"/>
                <attr name="instantiatingIterator" 
                            methodvalue="org.netbeans.examples.modules.povsamples.empty.EmptyPovrayProjectWizardIterator.createIterator"/>
                <attr name="instantiatingWizardURL" 
                            urlvalue="nbresloc:/org/netbeans/examples/modules/povsamples/empty/EmptyPovrayProjectDescription.html"/>
                <attr name="template" boolvalue="true"/>
            </file>
            <file name="SamplePovrayProjectProject.zip" url="SamplePovrayProjectProject.zip">
                <attr name="SystemFileSystem.localizingBundle" 
                            stringvalue="org.netbeans.examples.modules.povsamples.Bundle"/>
                <attr name="instantiatingIterator" 
                            methodvalue="org.netbeans.examples.modules.povsamples.sample.SamplePovrayProjectWizardIterator.createIterator"/>
                <attr name="instantiatingWizardURL" 
                            urlvalue="nbresloc:/org/netbeans/examples/modules/povsamples/sample/SamplePovrayProjectDescription.html"/>
                <attr name="template" boolvalue="true"/>
            </file>
        </folder>
    </folder>
</folder>

The above XML looks a lot to chew on, but it's relatively simple. For each template, we declare a file in Templates/Project/Standard: this means that these files will be findable at runtime in the System Filesystem (a kind of virtual configuration data store composed from XML layer files, which uses the same Filesystem interface you use in NetBeans modules to access files on disk). The attributes are key/value pairs that have some special meanings when applied to a template:

  • SystemFilesystem.localizingBundle: a pointer to a resource bundle (Properties file) that contains a localized, human-friendly, translatable name for the file.
  • instantiatingIterator: a pointer to a static method that can create a WizardIterator: a factory for additional pages in the New Project wizard.
  • instantiatingWizardURL: a pointer to an HTML file, which contains a description of the template, which will appear in the New Project dialog when the user selects our template.

Since these templates are nearly identical, we don't need separate classes for the New Project wizard for each one: one class will do nicely for both templates. So we will make one change to the layer file, and delete some of the classes that were generated:

  • Open layer.xml in the code editor. Find the line:
    <attr name="instantiatingIterator"
        methodvalue="org.netbeans.examples.modules.povsamples.empty.EmptyPovrayProjectWizardIterator.createIterator"/>
    and copy it to the clipboard (Ctrl-C).
  • Find the equivalent line in the definition for SamplePovrayProjectProject.zip, which declares the "instantiatingIterator" and select it.
  • Paste the line on the clipboard over this line, replacing it, so the line declaring the "instantiatingIterator" is the same for both entries.
  • Delete SamplePovrayProjectPanelVisual.java, SamplePovrayProjectWizardIterator.java and SamplePovrayProjectWizardPanel.java.

4.4 Modifying the Build Script

Our initial sample projects are probably not in their final form, so it would be nice to have the build script automatically rebuild the ZIP files of the sample projects whenever we build the Povray Projects module: that way we can simply modify the sample sources at will, and whenever we do a build they will be up-to-date. So we'll make a few changes to the build script:

Add the following targets to the Ant build script for Povray Project Samples:

<target name="netbeans" depends="package-samples,projectized-common.netbeans"/>

<target name="package-samples">

    <delete file="${basedir}/src/org/netbeans/examples/modules/povsamples/EmptyPovrayProjectProject.zip"/>
    <delete file="${basedir}/src/org/netbeans/examples/modules/povsamples/SamplePovrayProjectProject.zip"/>

    <zip compress="9" basedir="src/org/netbeans/examples/modules/povsamples/templates/EmptyPovrayProject/"
         zipfile="${basedir}/src/org/netbeans/examples/modules/povsamples/EmptyPovrayProjectProject.zip"/>
    <zip compress="9" basedir="src/org/netbeans/examples/modules/povsamples/templates/SamplePovrayProject/"
         zipfile="${basedir}/src/org/netbeans/examples/modules/povsamples/SamplePovrayProjectProject.zip"/>

</target>    

If we were using a version control such as CVS to store our source code, now would be a good time to mark the two ZIPs as ignored (add them to .cvsignore or equivalent).

Homework

Create a project in NetBeans IDE. It can be any kind of project, either a Swing project, or a web application, or a mobile application, anything at all. Or create a project of the kind you provided support for last week. Now turn that project into a project template! Question to think about: what is the difference between a project type, a project template, and a project sample?

Create a JUnit test for the code created during this week. Which pieces of code does it make sense to test? To help you, read the "Writing tests" section of the NetBeans Developer FAQ.

Next Week

Next week we will create the API needed for communication between our two current modules.


return to the topics



Week 5: Creating an API

As discussed when we designed POV-Ray project support, we will need an API: there will be some intercommunication between POV-Ray files and the project.

Resources

Before continuing with the lesson, do everything described in these two tutorials :

Conceptually, the above two tutorials cover the most important concepts relating to the NetBeans Platform.

Video

Watch this video, which will give you a basis for understanding the Lookup API in the NetBeans Platform:

Then read this article and follow the provided instructions: How Do NetBeans Extension Points Work?

Lab

5.1 Creating the API

We will need some interfaces:

  • MainFileProvider: find the main file of a project: the one to render when the whole file is built, and allow a POV-Ray scene file node to find out if it is the main file (so it can bold-face its display name).
  • RendererService: an API a POV-Ray file node can call to ask that it be rendered as an image.
  • ViewService: an API a POV-Ray file can call to ask that its associated image be shown in the IDE, rendering it if necessary.

For this, we will actually create a separate module. That way we avoid a dependency between file support and project support: either module will be loadable by itself as long as the module providing the API is there. Also, this helps in delivering updates: the API presumably will remain stable, and psychologically having it in a separate module helps the developer to be aware when they are making API changes. It also means that a completely different module supporting POV-Ray files could still get them rendered via this API, and completely different project support could be provided with no changes to the file support module. So it's generally healthy for the codebase to do it this way.

  1. Right-click the Modules node and choose Add New:

  2. Name the project "api". Make sure the Module Suite list is pointing to the POV-Ray module suite:

    Click Next or press Enter.

  3. Set the "Code Name Base" to "org.netbeans.examples.api.povray": this follows the NetBeans package naming conventions that public packages shall have the name org.netbeans.api... to indicate visually that they are intended to be API (and thus kept backward compatible). Provide the display name "Povray API":

  4. Click Finish or press Enter to create the project.

  5. Make sure that your module suite structure is as follows:

  6. In the Projects window, right-click "Povray API", which is the newly created project, and choose Properties. In the Libraries page, add a new dependency on the File System API module (look for AbstractFileSystem for a fast way to find it):

    Click OK and then click OK again.

  7. Create a new abstract Java class in org.netbeans.examples.modules.api.povray called MainFileProvider, and implement it as follows:

    public abstract class MainFileProvider {
    
        public abstract FileObject getMainFile();
        
        public abstract void setMainFile (FileObject file);
        
        public boolean isMainFile (FileObject obj) {
            return obj.equals(getMainFile());
        }
        
    }

    When you fix imports, make sure to import org.openide.filesystems.FileObject.

  8. Create a new abstract Java class in org.netbeans.modules.examples.api.povray called RendererService, and implement it as follows:

        public static final String PROJECT_RENDERER_KEY_PREFIX = "renderer.";
        public static final String PRODUCTION_RENDERER_SETTINGS_NAME = "production";
        public abstract FileObject render(FileObject scene, String propertiesName);
        public abstract FileObject render (FileObject scene, Properties renderSettings);
        public abstract FileObject render (FileObject scene);
        public abstract FileObject render();
        public abstract String[] getAvailableRendererSettingsNames();
        public abstract Properties getRendererSettings (String name);
        public abstract String getPreferredRendererSettingsNames();
        public abstract String getDisplayName (String settingsName);
        

    When you fix imports, make sure to import org.openide.filesystems.FileObject.

  9. Create a new Java interface in org.netbeans.modules.examples.api.povray called ViewService, and implement it as follows:

        boolean isRendered (FileObject file);
        boolean isUpToDate (FileObject file);
        void view (FileObject file);
    

    When you fix imports, make sure to import org.openide.filesystems.FileObject.

    If you are wondering why the first two are abstract classes instead of interfaces, the answer is simple: In the case of MainFileProvider, it allows us to implement isMainFile(); in the case of the other RendererService, it is highly probably that there will be new requirements for it in the future, and you can add methods [with some sort of default implementation] to an abstract class semi-backward-compatibly [name collisions with subclasses are still possible], but not to an interface. ViewService is simple and well-defined enough that it will probably never change.

  10. Right-click the newly created project in the Projects window, and choose Properties. Go to the API Versioning page in the dialog. Click the Autoload radio button: this means this module is a library: it will only be loaded if something else starts to use a class from it, which is more efficient. Also select the checkbox next to the package in the Public Packages list. You should now see the following:

    You have now exposed the package containing your API classes to other modules within the suite. Click OK to dismiss the Project Properties dialog.

  11. Right click the Povray Projects project and use the Libraries page to add a dependency on our new module: just search for one of the classes we've added:

  12. Next, do the same for the Povray File Support module.

Now both of the modules that need them can see the API classes, but not each others' classes.

5.2 Using the API from PovrayDataNode

We haven't implemented the API yet, but we can set up some code that will use it: we know we want the node for the file which is the "main file" of our project to be shown in bold text. And having some code that uses the API will help to test it once it is written, which will be a bit of work.

  1. Right-click the Povray File Support project, choose Properties, and open the Libraries page of the Project Properties dialog. Click the Add Dependency button, and in the dialog type "FileOwnerQuery": we are adding a dependency on the Project API:

    FileOwnerQuery is part of that API: a class with static methods that will return the project (if any) which owns a given file. Our Node will need to look up the project it belongs to, and then query the project's Lookup to try to find an implementation of our API classes.

  2. In Povray File Support project, create a new Java class called PovrayDataNode in the org.netbeans.examples.modules.povfile package. Add the following content to the new PovrayDataNode class:

    public class PovrayDataNode extends DataNode {
    
        public PovrayDataNode(PovrayDataObject obj, Lookup lookup) {
            super(obj, Children.LEAF, lookup);
        }
    
        private FileObject getFile() {
            return getDataObject().getPrimaryFile();
        }
    
        private Object getFromProject (Class clazz) {
            Object result;
            Project p = FileOwnerQuery.getOwner(getFile());
            if (p != null) {
                result = p.getLookup().lookup (clazz);
            } else {
                result = null;
            }
            return result;
        }
    
        private boolean isMainFile() {
            MainFileProvider prov = (MainFileProvider)getFromProject (MainFileProvider.class);
            boolean result;
            if (prov == null) {
                result = false;
            } else {
                FileObject myFile = getFile();
                result = prov.isMainFile(myFile);
            }
            return result;
        }
    
        @Override
        public String getHtmlDisplayName() {
            return isMainFile() ? "<b>" + getDisplayName() + "</b>" : null;
        }
    
    }

    What the above code does is fairly straightforward:

    • getFile() returns a FileObject (NetBeans virtual filesystem file) that this Node represents.

    • getFromProject tries to find the project that owns the file, and if it finds one, queries its Lookup, asking it for an instance of the Class that was passed into this method (e.g. one of the classes in the API we just defined).

    • isMainFile() uses the above two methods to decide if this Node represents the "main file" of the project (the one that should be rendered by POV-Ray if the user chooses to "build" the project: POV-Ray supports file includes, so there may be many files in a project, but only one master image).

    • getHtmlDisplayName() is where the rubber meets the road: this method will return a boldface HTML string if this Node represents the main file.
  3. Open PovrayDataObject and use the createNodeDelegate method to return the above node, instead of the default that the File Type wizard created for you earlier:

    @Override
    protected Node createNodeDelegate() {
        return new PovrayDataNode(this, getLookup());
        //return new DataNode(this, Children.LEAF, getLookup());
    }


Homework

The two tutorials you worked through at the start of this week are completed by these two. They're very important as well, covering essential features of the NetBeans Platform. Do them for homework and try to understand everything you do:

To prove to yourself that you understand the concepts presented inthe ne the two tutorials above, use the Nodes API to create a generic hierarchy over a set of data (any kind of data) and then use the Explorer & Property Sheet API to provide a view on top of your nodes to the end user.

How would you use these APIs outside of the NetBeans Platform? What would be the benefits of doing so? Can you create a scenario that shows the benefits of taking this approach?

Next Week

Next week we will implement the API we have created.


return to the topics



Week 6: Implementing the API

This week, you will implement the API that you created in the previous lesson. You will be coding only within the Povray Projects project throughout this week: all the other modules will remain unchanged. (Think about why that might be a good thing and what the benefits of this are in a distributed development environment!)

Resources

Before beginning this lesson, spend some time getting to know the Visual Library API. It is not used within this lesson, but spending some time with this API will give you a good basis for creating visual applications. That's something that the NetBeans Platform helps you with in many ways, as you will find out during these tutorials. Do at least two of the tutorials below and make sure to understand everything you're doing as you're doing it:

Click on the image above to get to the Learning Trail where the listed tutorials are found.

Video

Watch this video, which will give you a basis for understanding the System Filesystem, which you will be using this week:

Lab

6.1 Implementing MainFileProvider

The first class we will implement is MainFileProvider. This is the class that our Nodes for POV-Ray files will look up, to see if they represent the main scene file of the project (if so they will display their name in boldface text, and later this will be used to provide actions to set which file is the main file).

  1. In the Povray Projects project, create a new Java class in the package org.netbeans.examples.modules.povproject, and call it "MainFileProviderImpl":

  2. Implement your new class as follows:

    class MainFileProviderImpl extends MainFileProvider {
    
        private final PovrayProject proj;
        private FileObject mainFile = null;
        private boolean checked = false;
    
        MainFileProviderImpl(PovrayProject proj) {
            this.proj = proj;
        }
    
        public FileObject getMainFile() {
            //Try to look up the main file in the project properties
            //the first time this is called;  no need to look it up every
            //time, either it's there or it's not and when the user sets it
            //we'll save it when the project is closed
            if (mainFile == null && !checked) {
                checked = true;
                Properties props = (Properties) proj.getLookup().lookup(
                        Properties.class);
    
                String path = props.getProperty(PovrayProject.KEY_MAINFILE);
                if (path != null) {
                    FileObject projectDir = proj.getProjectDirectory();
                    mainFile = projectDir.getFileObject(path);
                }
            }
            if (mainFile != null && !mainFile.isValid()) {
                return null;
            }
            return mainFile;
        }
    
        public void setMainFile(FileObject file) {
            String projPath = proj.getProjectDirectory().getPath();
            assert file == null ||
                    file.getPath().startsWith(projPath) :
                    "Main file not under project";
    
            boolean change = ((mainFile == null) != (file == null)) ||
                    (mainFile != null && !mainFile.equals(file));
    
            if (change) {
                mainFile = file;
                //Get the project properties (loaded from
                //$PROJECT/pvproject/project.properties)
                Properties props = (Properties) proj.getLookup().lookup(
                        Properties.class);
    
                //Store the relative path from the project root as the main file
                String relPath = file.getPath().substring(projPath.length());
                props.put(PovrayProject.KEY_MAINFILE, relPath);
            }
        }
    }

    The code above is simple and quite straightforward: it will look for a Properties object in the Lookup of the project. The getter will look for the value of "main.file" from the Properties object (which was loaded from $PROJECT/pvproject/project.properties), which will be a relative path to the main file. If there is a value for that key, try to find the corresponding file and return it. The setter, in turn, will write a new relative path to the Properties object. That in turn, will cause the project to be marked as modified, so the system will call PovProjectFactory.saveProject() if it is unloading the project, causing the Properties to be written out to disk in $PROJECT/pvproject/project.properties.

  3. Add the key for the main file to the top of PovrayProject:

    public static final String KEY_MAINFILE = "main.file";

  4. Add new MainFileProviderImpl(this) to the implementation of getLookup() in PovrayProject, so that it is included in the array of objects that make up the lookup contents:

    public Lookup getLookup() {
        if (lkp == null) {
            lkp = Lookups.fixed(new Object[]{
                this, //project spec requires a project be in its own lookup
                state, //allow outside code to mark the project as needing saving
                new ActionProviderImpl(), //Provides standard actions like Build and Clean
                loadProperties(), //The project properties
                new Info(), //Project information implementation
                logicalView, //Logical view of project implementation
                new MainFileProviderImpl(this),
            });
        }
        return lkp;
    }

6.2 Providing Set Main File & Render Menu Items on POV-Ray Files

Now we have an implementation of some of our API, the next step is to use it. As discussed earlier, we want the user to be able to right-click and choose to render any file, not just the main file of the project. So there should be some menu items available from our PovrayDataNodes which will allow the user to render the file with one of our sets of settings.

  1. Open PovrayDataNode, from the Povray File Support project, in the code editor.

    Press Alt-Insert and override the getActions(boolean) method. Implement it as follows:

    @Override
    public Action[] getActions(boolean popup) {
        Action[] actions = super.getActions(popup);
        Action[] result;
        result = new Action[actions.length + 1];
        result[0] = actions[0];
        result[1] = new SetMainFileAction();
        System.arraycopy(actions, 1, result, 3, actions.length - 1);
        return result;
    }

    This method will add one (yet to be implemented) action into the array of actions. It positions it as the second element in the array, since the first element is what will be invoked when you double click the file, and we want that to remain for opening the file (although we could also override getPreferredAction() to determine what happens when the node is doubled clicked).

  2. We will simply implement this as an inner class of PovrayDataNode. Open PovrayDataNode in the code editor.

    Implement it as follows. The only twist is that if our Node becomes the main file, it needs to tell the former main file that it is not the main file anymore: more specifically, it needs to force it to fire a property change in its display name so that it gets redrawn as non-bold:

        private final class SetMainFileAction extends AbstractAction {
    
            public SetMainFileAction() {
                //Set a display name
                putValue(Action.NAME, NbBundle.getMessage(PovrayDataNode.class,
                        "LBL_MainFileAction"));
            }
    
            public void actionPerformed(ActionEvent ae) {
                MainFileProvider provider = (MainFileProvider) getFromProject(MainFileProvider.class);
                FileObject oldMain = provider.getMainFile();
                provider.setMainFile(getFile());
                fireDisplayNameChange(getDisplayName(), getHtmlDisplayName());
                if (oldMain != null) {
                    try {
                        Node oldMainFilesNode = DataObject.find(oldMain).getNodeDelegate();
                        if (oldMainFilesNode instanceof PovrayDataNode) {
                            ((PovrayDataNode) oldMainFilesNode).fireDisplayNameChange(null, oldMainFilesNode.getDisplayName());
                        }
                    } catch (DataObjectNotFoundException donfe) { //Should never happen
                        Exceptions.printStackTrace(donfe);
                    }
                }
            }
    
            @Override
            public boolean isEnabled() {
                return !isMainFile() && getFromProject(MainFileProvider.class) != null;
            }
        }
            

  3. The last step is to add the localized name of the action to the Bundle.properties file in the same package, i.e., in org.netbeans.examples.modules.povfile: add this text to that file:

    LBL_MainFileAction=Set Main File

  4. With that, you should be able to clean, build and run the module suite, and be able to set the main file in a project. Do so by right-clicking a file in a POV-Ray project, in the Projects window, and choose "Set Main File":

    When you do so, the name of the file is displayed in bold:

    When you set a different file as the main file, the original main file no longer has its name displayed in bold:

  5. Let's now add another action, for rendering (changes to code shown in bold):

    @Override            
    public Action[] getActions (boolean popup) {
        Action[] actions = super.getActions(popup);
        RendererService renderer = (RendererService)getFromProject (RendererService.class);
        Action[] result;
        if (renderer != null && actions.length > 0) { //should always be > 0
            Action rendererAction = new RendererAction (renderer, this);
            result = new Action[ actions.length + 2 ];
            result[0] = actions[0];
            result[1] = new SetMainFileAction();
            result[2] = rendererAction;
            System.arraycopy(actions, 1, result, 3, actions.length-1);
        } else {
            //Isolated file in the favorites window or something:
            result = actions;
        }
        return result;
    }
            

    This method will add another (yet to be implemented) action into the array of actions, if a renderer service for this file can be found.

  6. Now we need to implement RendererAction. Right click the org.netbeans.examples.modules.povfile package, and create a new Java class called RendererAction. Define its signature as follows:

    public class RendererAction extends AbstractAction implements Presenter.Popup {

    Implementing Presenter.Popup is an important step: this is a way in which an action can actually provide whatever component it wants to insert into the popup menu. It is a one-method interface, with the method getPopupPresenter which returns a JMenuItem (remember that in Swing, JMenu is a subclass of JMenuItem, so it's legal to return whole submenus here). In our case, we want a submenu:

    • Render
      • 1024 x 768 High Quality
      • 1024 x 768
      • 640 x 480 High Quality
      • 640 x 480 High Quality
      • 320 x 200
      • 160 x 120
    • Standard file menu items...

  7. Now we will provide the body of RendererAction:

                
        private final RendererService renderer;
        private final PovrayDataNode node;
    
        public RendererAction(RendererService renderer, PovrayDataNode node) {
            this.renderer = renderer;
            this.node = node;
        }
    
        public void actionPerformed(ActionEvent e) {
            assert false;
        }
    
        public JMenuItem getPopupPresenter() {
        
            JMenu result = new JMenu ();
    
            //Set the menu's label
            result.setText (NbBundle.getMessage (RendererAction.class, "LBL_Render"));
    
            //Get the names of all available settings sets:
            String[] availableSettings = renderer.getAvailableRendererSettingsNames();
    
            //Get the name of the most recently used setting set:
            String preferred = renderer.getPreferredRendererSettingsNames();
    
            for (int i = 0; i < availableSettings.length; i++) {
            
                String currName = availableSettings[i];
                
                RenderWithSettingsAction action = new RenderWithSettingsAction(currName);
    
                JCheckBoxMenuItem itemForSettings = new JCheckBoxMenuItem (action);
    
                //Show our menu item checked if it is the most recently used set of settings:
                itemForSettings.setSelected (preferred != null && preferred.equals(currName));
    
                result.add (itemForSettings);
                
            }
            
            return result;
            
        }
    }
            

  8. The one thing missing here, of course, are the individual actions that will run the renderer with different sets of settings. Create an inner class of RendererAction called RenderWithSettingsAction, and implement it as follows:

    private class RenderWithSettingsAction extends AbstractAction implements Runnable {
        private final String name;
        public RenderWithSettingsAction (String name) {
            this.name = name;
            putValue (NAME, renderer.getDisplayName(name));
        }
    
        public void actionPerformed(ActionEvent e) {
            RequestProcessor.getDefault().post(this);
        }
    
        public void run() {
            DataObject ob = node.getDataObject();
            FileObject toRender = ob.getPrimaryFile();
            FileObject image = renderer.render(toRender, name);
            if (image != null) {
                try {
    
                    //Try to open the file:
                    DataObject dob = DataObject.find (image);
                    Node n = dob.getNodeDelegate();
                    OpenCookie ck = (OpenCookie) n.getLookup().lookup(
                            OpenCookie.class);
                    if (ck != null) {
                        ck.open();
                    }
                } catch (DataObjectNotFoundException e) {
                    //Should never happen
                    Exceptions.printStackTrace(e);
                }
            }
        }
    }
            

    This is relatively straightforward as well. We are using the renderer field of the outer class, and only storing the name of which specific Properties file should be used to provide settings for this class, which we will pass to renderer.render().

    The two interesting areas are how we find the file, and how we actually perform the rendering. We have the instance of PovRayDataNode that we are operating against. It is a subclass of DataNode, so we can call its getDataObject() method (another way would be to call node.getLookup().lookup(DataObject.class), but since we know its type, calling getDataObject() is more efficient). From that we may call getPrimaryFile() to actually get the FileObject that should be rendered into an image by POV-Ray.

    The other item of interest is how we do our rendering. Notice that we implement Runnable. Our action will be, by default, called from the event dispatch thread when the user clicks it in a menu. It would not be good at all if running the action blocked the UI from repainting or anything else until the external POV-Ray process was completed. So instead, we use a handy thread pool NetBeans provides for us, and simply post the work to be done on another thread off of the event queue.

  9. The last step is to add the localized name of the action to the Bundle.properties file in the same package: add this text to that file:

    LBL_Render=Render

6.3 Implementing RendererService: Providing Default Renderer Settings

The next class to implement is RendererService: this is the service, belonging to the project, by which a project will be "compiled" into an image (by executing the POV-Ray program and passing it arguments).

As we discussed earlier, we are not going to try to implement a complicated dialog that makes every possible POV-Ray setting adjustable via a GUI widget: this would add a lot of complexity when many users would be satisfied with a reasonable set of defaults. So we will have a set of different default combinations of settings that should satisfy most users. Later we will add the ability to create completely customized settings by editing the project.properties of a POV-Ray project, to satisfy the needs of power users.

Right now, we will not worry about the execution part: RendererService also provides for named sets of settings: combinations of line switches which should be passed to POV-Ray to determine rendering quality, image size and speed. Right now we will only implement that part of RendererService.

This is where we will begin dealing directly with the System Filesystem: the registry of runtime data supplied by modules. What we will do is create .properties files for each set of standard settings we will supply. We will start by creating .properties files for each set of settings in our module.

  1. Create a new Java Package in the Povray Projects project, with the name org.netbeans.examples.modules.povproject.defaults. This is where we will put our Properties files. Within that package, create 6 Properties files with the following name & contents:

    160x100.properties:

    W=160
    H=100
    Q=4
    FN=8
    A=0.0
          

    320x200.properties:

    W=320
    H=200
    Q=4
    FN=8
    A=0.0
          

    640x480.properties:

    W=640
    H=480
    Q=4
    FN=8
    A=0.0
          

    640x480hq.properties:

    W=640
    H=480
    Q=R
    FN=8
    A=0.9
          

    1024x768.properties:

    W=1024
    H=768
    Q=4
    FN=9
    A=0.0
          

    1024x768hq.properties:

    W=1024
    H=768
    Q=R
    FN=8
    A=0.9
          

  2. Make sure that the project source structure is as follows:

  3. Next, we will want to actually add these to the System Filesystem, so our module can find them at runtime, and more importantly, so other modules can modify and save, or add additional, sets of default settings by adding more Properties files to the same folder we put these files in, in the System Filesystem. Open the layer.xml file for the Povray Projects project in the code editor.

    Add the following content anywhere within the <filesystem> tags in the Povray Projects project's layer file:

    <folder name="Povray">
        <folder name="RendererSettings">
            <file name="1024x768hq.properties" url="defaults/1024x768hq.properties"> 
                <attr name="SystemFileSystem.localizingBundle" 
                      stringvalue="org.netbeans.examples.modules.povproject.defaults.Bundle"/> 
                <attr name="position" intvalue="100"/>
            </file>
            <file name="1024x768.properties" url="defaults/1024x768.properties"> 
                <attr name="SystemFileSystem.localizingBundle" 
                      stringvalue="org.netbeans.examples.modules.povproject.defaults.Bundle"/> 
                <attr name="position" intvalue="200"/>
            </file> 
            <file name="640x480hq.properties" url="defaults/640x480hq.properties"> 
                <attr name="SystemFileSystem.localizingBundle" 
                      stringvalue="org.netbeans.examples.modules.povproject.defaults.Bundle"/> 
                <attr name="position" intvalue="300"/>
            </file> 
            <file name="640x480.properties" url="defaults/640x480.properties"> 
                <attr name="SystemFileSystem.localizingBundle" 
                      stringvalue="org.netbeans.examples.modules.povproject.defaults.Bundle"/> 
                <attr name="position" intvalue="400"/>
            </file> 
            <file name="320x200.properties" url="defaults/320x200.properties"> 
                <attr name="SystemFileSystem.localizingBundle" 
                      stringvalue="org.netbeans.examples.modules.povproject.defaults.Bundle"/> 
                <attr name="position" intvalue="500"/>
            </file> 
            <file name="160x100.properties" url="defaults/160x100.properties"> 
                <attr name="SystemFileSystem.localizingBundle" 
                      stringvalue="org.netbeans.examples.modules.povproject.defaults.Bundle"/> 
                <attr name="position" intvalue="600"/>
            </file> 
        </folder> 
    </folder>

    What this XML does is map the Properties files we just created into the System Filesystem in the folder Povray/RendererSettings, which is where our code will look for them. Additionally, it specifies ordering attributes, which are attributes we are adding to the folder RendererSettings/, which will determine what order the files will appear in when code asks for the array of children of the DataFolder (DataObject subclass for folders) or its Node for this folder.

    You may have noticed the attribute SystemFilesystem.localizingBundle which we added to the RendererSettings folder. NetBeans FileObjects (which is what the "files" in the System Filesystem are) can have ad-hoc key-value pairs associated with them. SystemFilesystem.localizingBundle is a magic attribute which the system will use to localize the names of files: all you have to do is get the DataObject for a file in the System Filesystem, get the Node for that DataObject, and the return value of Node.getDisplayName() for that Node to look up its localized display name in the requested resource bundle: this is how file names for things declared in the System Filesystem are localized.

    So we need one more Properties file in org.netbeans.examples.modules.povproject.defaults: create one called "Bundle". This one won't contain renderer defaults, it will contain mappings from the file names of the files we declared above, to their localized, human friendly names.

  4. Create a Bundle.properties file in org.netbeans.examples.modules.povproject.defaults (as specified by the SystemFileSystem.localizingBundle in the layer file) and add the following contents to it:

    Povray/RendererSettings/1024x768.properties=1024 x 768
    Povray/RendererSettings/1024x768hq.properties=1024 x 768 High Quality
    Povray/RendererSettings/640x480hq.properties=640 x 480 High Quality
    Povray/RendererSettings/640x480.properties=640 x 480
    Povray/RendererSettings/320x200.properties=320 x 200
    Povray/RendererSettings/160x100.properties=160 x 100

6.4 Implementing RendererService: Basic Implementation

Now we have a set of default settings to show, so we can implement the methods of RendererService that will expose them.

  1. Create a new final class, RendererServiceImpl, in org.netbeans.examples.modules.povproject:

  2. Modify the class declaration to say that it extends RendererService and press Ctrl-Shift-I to fix imports.

    The first thing we will do is implement the constructor:

    final class RendererServiceImpl extends RendererService {
    
        private PovrayProject proj;
    
        public RendererServiceImpl(PovrayProject proj) {
            this.proj = proj;
        }
    
        PovrayProject getProject() {
            return proj;
        }

    We will leave the render methods stubbed out: we will implement these later.

  3. Next, we will add some private utility methods that the other methods will use. This should help to give some of the flavor of working with things in the System Filesystem.

       private static boolean logged = false;
                
       private FileObject getRendererSettingsFolder() {
    
            String folderName = "Povray/RendererSettings/";
    
            FileSystem systemFileSystem =
                    Repository.getDefault().getDefaultFileSystem();
    
            FileObject result =
                    systemFileSystem.getRoot().getFileObject(folderName);
    
            if (result == null && !logged) {
                //Corrupted userdir or something is very very wrong.
                //Log it and move on.
                Exceptions.printStackTrace(new IllegalStateException("Renderer settings dir missing!"));
                logged = true;
            }
    
            return result;
    
        }
    
        private FileObject fileFor (String settingsName) {
    
            FileObject settingsFolder = getRendererSettingsFolder();
    
            FileObject result;
    
            if (settingsFolder != null) { //should never be null
                result = settingsFolder.getFileObject(settingsName);
            } else {
                result = null;
            }
    
            return result;
    
        }
    
        static Preferences getPreferences() {
            return NbPreferences.forModule(RendererServiceImpl.class);
        }

    • The first thing we have is a utility method that finds the folder we declared in our XML layer, in the System Filesystem: that is what getRendererSettingsFolder() does. You'll note that there is a null check. This folder should not be null, since we are declaring it in our layer. But it conceivably could be (a corrupted settings directory or a module that for some reason hides the settings directory: it should not happen, but it is theoretically possible), so we log an exception if so, rather than throwing exceptions every time something goes and looks for a display name for a menu item or similar.

      In NetBeans Platform 7.0, you will be able to use FileUtil.getConfigFile(folderName) instead of Repository.getDefault().getDefaultFileSystem().getRoot().getFileObject(folderName).

    • The next method is a utility method that just fetches the file corresponding to a file name: we are returning the names of all files in the settings folder, so this will allow us to find a corresponding Properties file.

    • The last method is simply for loading and storing preferences, such as the last-used set of renderer settings, as well as the povray executable and POV-Ray's directory of include files. We could return Preferences.userNodeForPackage(RendererServiceImpl.class) here, but instead we use the NetBeans NbPreferences class, a NetBeans-specific API that will store and load the setting in the suite's NetBeans user directory, instead of the JDK's general location for settings.

  4. The first method we want to implement is getAvailableRendererSettingsNames(). This method will return an array of Strings: the localized, human-friendly names of all of the files which we declared above:

        @Override    
        public String[] getAvailableRendererSettingsNames() {
            FileObject settingsFolder = getRendererSettingsFolder();
            String[] result;
            if (settingsFolder != null) {
                //Use a DataFolder here, so our ordering attributes in the layer
                //file are applied, and our returned String array will be in the
                //order we want
                DataFolder fld = DataFolder.findFolder(settingsFolder);
                DataObject[] kids = fld.getChildren();
                result = new String[ kids.length ];
                for (int i = 0; i < kids.length; i++) {
                    result[i] = kids[i].getPrimaryFile().getNameExt();
                }
            } else {
                result = new String[0];
            }
            return result;
        }

    This is quite straightforward: we just iterate all of the files in the folder, and return an array of Strings with their names. The one twist to it is that we don't iterate the FileObject's children, but rather we get a DataFolder (the DataObject type for filesystem folders), and iterate its children. The reason we do it this way is that the order of children of FileObjects is undefined: we might get the files we declared in any order. The DataFolder, however, understands ordering attributes: attributes we can declare in the XML of our layer file, which will determine what order a folder's children are returned in. So this enables us to sort our settings files in an intuitive way: yet other modules could still insert additional settings, with their own ordering attributes, and they would be included in the sort (for more info on how and why this works, see the Javadoc for Utilities.topologicalSort()). Also note that you can localize the name of a DataObject, but not a FileObject.

  5. Next we will implement getRendererSettings(name): this method will actually get a Properties object with the contents of whichever file name was passed to it:

        @Override
        public Properties getRendererSettings(String name) {
            Properties result = new Properties();
            FileObject settingsFile = fileFor(name);
            if (settingsFile != null) {
                try {
                    result.load(new BufferedInputStream(settingsFile.getInputStream()));
                } catch (FileNotFoundException ex) {
                    Exceptions.printStackTrace(ex);
                } catch (IOException ex) {
                    Exceptions.printStackTrace(ex);
                }
            } else {
                Exceptions.printStackTrace(new IllegalStateException
                        ("Requested non-existent settings " +
                        "file " + name));
            }
            return result;
        }
        

    The code here is also quite straightforward: it simply tries to load a Properties object from the input stream of the file in question.

  6. Next we will implement the method that fetches the name of the preferred set of settings: this will be the most recently used settings, fetched from the NbPreferences API, with a fallback if none has yet been chosen:

        private String KEY_PREFERRED_SETTINGS = "";
    
        @Override    
        public String getPreferredRendererSettingsNames() {
            String result = getPreferences().get(KEY_PREFERRED_SETTINGS, null);
            if (result == null) {
                result = "640x480.properties";
            }
            return result;
        }
        

  7. The last method we will implement takes a settings file name and converts it to a localized, human-readable name:

        @Override    
        public String getDisplayName(String settingsName) {
            FileObject file = fileFor (settingsName);
            String result;
            if (file != null) {
                DataObject dob;
                try {
                    dob = DataObject.find(file);
                    result = dob.getNodeDelegate().getDisplayName();
                } catch (DataObjectNotFoundException ex) {
                    Exceptions.printStackTrace(ex);
                    result = "[error]";
                }
            } else {
                result = "";
            }
            return result;
        }
        

    Human-readable display names are provided by Nodes: a FileObject is simply a file on disk (or similar storage such as the System Filesystem via our layer.xml file): it has no notion of human readability. So if we want the localized name for a FileObject, we need to get the Node for it. In this case, the Node will use the hint we provided in the layer.xml file:

    <attr name="SystemFileSystem.localizingBundle"
          stringvalue="org.netbeans.modules.povproject.defaults.Bundle"/>

    and look up its localized name in org.netbeans.examples.modules.povproject.defaults.Bundle.properties.

  8. Now we just need to expose our implementation of RendererService via the project's lookup. Modify PovrayProject.getLookup() as follows:

    public Lookup getLookup() {
        if (lkp == null) {
            lkp = Lookups.fixed(new Object[] {
                this,  //project spec requires a project be in its own lookup
                state, //allow outside code to mark the project as needing saving
                new ActionProviderImpl(), //Provides standard actions like Build and Clean
                loadProperties(), //The project properties
                new Info(), //Project information implementation
                logicalView, //Logical view of project implementation
                new RendererServiceImpl(this), //Renderer Service Implementation
                new MainFileProviderImpl(this), //So things can set the main file
    
            });
        }
        return lkp;
    }

6.5 Locating the POV-Ray executable

The next step is to write the code that will actually run POV-Ray and send its text output to the Output window, and eventually open a rendered image. Since this involves some complicated code, we will create a separate utility class that will do the actual rendering.

  1. Create a new Java class in the Povray Projects project, in org.netbeans.examples.modules.povproject, called "Povray":

  2. First we need to implement support for finding the POV-Ray executable, so that we have something to run. This will simply be a matter of popping up a JFileChooser to let the user locate the POV-Ray executable and the directory with the standard POV-Ray include files: once this has been done once, we will store the result so we do not have to ask again unless it is deleted.

    Since we may need a file chooser twice, once to locate the executable, and once to locate the standard include file directory (which contains files that define standard colors, shapes, etc. that can be used in POV-Ray files), we should provide one method that shows a file chooser for both cases. Add the following method to Povray:

    private static File locate(String key) {
        try {
            final JFileChooser jfc = new JFileChooser();
            jfc.setDialogTitle(NbBundle.getMessage(Povray.class, key));
            jfc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
            SwingUtilities.invokeAndWait(new Runnable() {
    
                public void run() {
                    jfc.showOpenDialog(WindowManager.getDefault().getMainWindow());
                }
            });
            File result = jfc.getSelectedFile();
            return result;
        } catch (InterruptedException ex) {
            Exceptions.printStackTrace(ex);
        } catch (InvocationTargetException ex) {
            Exceptions.printStackTrace(ex);
        }
        return null;
    }
    

  3. At this point we need to add another dependency, because we are calling WindowManager. That is part of the Window System API. We could pass null here, but then there is the risk that on some window managers, our file chooser would pop up behind the main window. This makes sure it stays on top.

    Add a dependency on the Window System API to Povray Projects using the Project Properties dialog:

  4. As you can see in the above code, we will be fetching a localized string from a resource bundle: a different one depending on whether we're looking for the executable or include directory. So let's add those strings to the resource bundle for this package now. We will also add one warning message we will need later.

    Open org.netbeans.examples.modules.povproject.Bundle.properties in the code editor, and add the following key/value pairs:

    TTL_FindPovray=Locate POV-Ray Executable
    TTL_FindIncludeDir=Find POV-Ray Standard Include File Dir
    
    MSG_WindowsWarning=<html>POV-Ray for Windows always displays its graphical \
        user interface when it runs. You can get a command-line version of \
        POV-Ray at <a href="http://www.povray.org/beta/</a></html>
            

  5. Now we will add the two methods for fetching the POV-Ray executable and the include directory, which will automatically ask the user if they are unknown or unavailable. Add the following two methods, and their associated fields to Povray:

        private static File povray = null;
        private static File include = null;
    
        /** Preferences key for the povray executable */
        private static final String KEY_POVRAY_EXEC = "povray";
        /** Preferences key for the povray standard includes dir */
        private static final String KEY_POVRAY_INCLUDES = "include";
        
        private static File getPovray() {
            if (povray == null || !povray.exists()) {
                Preferences prefs = RendererServiceImpl.getPreferences();
                String loc = prefs.get(KEY_POVRAY_EXEC, null);
                if (loc != null) {
                    povray = new File (loc);
                }
    
                if (povray == null || !povray.exists()) {
                    File maybePov = locate("TTL_FindPovray");
    
                    if (maybePov.getPath().endsWith("pvengine.exe")) {
                        //Warn the user to get a command line build
                        NotifyDescriptor msg = new NotifyDescriptor.Confirmation (
                            NbBundle.getMessage(RendererServiceImpl.class,
                                "MSG_WindowsWarning"),
                            NotifyDescriptor.WARNING_MESSAGE);
    
                        Object result = DialogDisplayer.getDefault().notify(msg);
                        if (result == NotifyDescriptor.CANCEL_OPTION) {
                            return null;
                        }
                    }
                    povray = maybePov;
                    if (povray != null) {
                        //Here we set the povray executable path in a Properties file,
                        //in the NetBeans user directory, because we are
                        //using the NbPreferences API class:
                        prefs.put(KEY_POVRAY_INCLUDES, povray.getPath());
                    }
                }
            }
            return povray;
        }
    
    
        private static File getStandardIncludeDir (File povray) {
            if (include != null) {
                return include;
            }
            Preferences prefs = RendererServiceImpl.getPreferences();
            String loc = prefs.get(KEY_POVRAY_INCLUDES, null);
            if (loc != null) {
                include = new File (loc);
                if (!include.exists()) {
                    include = null;
                }
            }
            if (include == null) {
                include = new File (povray.getParent() +
                        File.separator + "include");
    
                if (!include.exists()) {
                    include = locate ("TTL_FindIncludeDir");
                    if (include != null) {
                        //Here we set the include path in a Properties file,
                        //in the NetBeans user directory, because we are
                        //using the NbPreferences API class:
                        prefs.put(KEY_POVRAY_INCLUDES, include.getPath());
                    } else {
                        include = null;
                    }
                }
            }
            return include;
        }
            

  6. Add a dependency on the Dialogs API in the Povray Projects project, using the Project Properties dialog:


Homework

Do all of the below:

  • In section 6.2 we talked about default renderer settings. Reimplement those defaults such that the user will be able to set them in the Options window, instead of on the node! Use the NetBeans Options Window Module Tutorial to help you.

  • In this lesson, in section 6.6, you used the NetBeans NotifyDescriptor class for the first time. Why would you use this class? Why/how is it preferable to using the JDK's JOptionPane class? Are there disadvantages to using this class?

  • So far, you've watched part 1 and part 2 of the video series "Top 10 NetBeans APIs". For homework, go to the Top 10 NetBeans APIs page and watch part 3, 4, and 5. Then create a NetBeans Platform application that builds an explorer view on top of a folder in the System Filesystem!

Next Week

Next week we will cover actually executing POV-Ray and piping its output to the Output window!


return to the topics



Week 7: Support For Running POV-Ray from NetBeans

At this point, we're almost ready to run code, so it would be a good idea to download a copy of POV-Ray. Official builds can be obtained from povray.org.

  • Windows. The Preparation document provides all the information that Windows users need, including the warning that you should not install POV-Ray in a directory path with spaces, even though the POV-Ray installer will suggest that you do so.

  • Mac. Mac users may find DarwinPorts the easiest way: simply install DarwinPorts and then run sudo port install povray.

  • Linux. Linux and other Unix users should be fine with the downloads available from povray.org. Everything should work out of the box for these users, without any tweaks or post-install configurations. Make sure, however, that the POV-Ray launcher has the correct permissions, otherwise it will not execute. Run it from the command line before continuing.

Resources

Before continuing with the lesson, read through and understand (and, where applicable, take the steps described) in the following documents:

  • Getting Started with POV-Ray: read through this document to understand what POV-Ray does. Try and follow the steps in POV-Ray and get used to the main concepts.
  • POV-Ray FAQ: read through these questions and answers to see what some common problems are that users of POV-Ray struggle with. (Then think about ways in which you might extend the project you're working on to cater to these problems!)

Don't continue until you have a global understanding of POV-Ray, its purpose, and how to work with it.

Lab

7.1 Obtaining POV-Ray

Obtaining POV-Ray is described in the Preparation document, as well as in the introduction to this lesson. Before continuining, make sure POV-Ray is set up correctly to avoid grief and frustration later this week.

7.2 Executing POV-Ray and Displaying the Output

At this point, we are ready to write the code that will actually invoke the external POV-Ray executable, pass it the correct arguments and display its output. POV-Ray has two kinds of output: it will write out status and success/failure on the command line, in a similar way to what a compiler does, and it will write out an image file on disk, which we will want to display.

The first part is just being able to invoke the external executable. The NetBeans Platform has some API classes that can help with that.

  1. Add the following constructor and fields to the Povray class:

        private final RendererServiceImpl renderService;
        private final FileObject toRender;
        private final Properties settings;
    
        Povray (RendererServiceImpl renderService, FileObject toRender, Properties settings) {
            this.renderService = renderService;
            this.toRender = toRender;
            this.settings = settings == null ? renderService.getRendererSettings(renderService.getPreferredRendererSettingsNames()) : settings;
        }

  2. Next we will implement a method that will find the file to render. We were passed a FileObject, but now we need an actual java.io.File to get the path from, to pass on the command line to POV-Ray. There are two caveats:
    1. The file passed to the constructor may be null: in that case we should find the main file of the project and use that.
    2. It is conceivable that the file will not exist on disk: NetBeans filesystems are virtual, after all, and the file could exist in a remote FTP filesystem or such. Since NetBeans 4.0, this is rather unlikely, but we should still test for this condition (FileUtil.toFile() returns null).

    So, we will add a method to Povray as follows:

    private File getFileToRender() throws IOException {
        FileObject render = toRender;
        if (render == null) {
            PovrayProject proj = renderService.getProject();
            MainFileProvider provider = (MainFileProvider)
                proj.getLookup().lookup (MainFileProvider.class);
            if (provider == null) {
                throw new IllegalStateException ("Main file provider missing");
            }
            render = provider.getMainFile();
            if (render == null) {
                ProjectInformation info = (ProjectInformation)proj.getLookup().lookup(ProjectInformation.class);
                //XXX let the user choose
                throw new IOException (NbBundle.getMessage(Povray.class, "MSG_NoMainFile", info.getDisplayName()));
            }
        }
        assert render != null;
        File result = FileUtil.toFile (render);
        if (result == null) {
                throw new IOException (NbBundle.getMessage(Povray.class, "MSG_VirtualFile", render.getName()));
        }
        assert result.exists();
        assert result.isFile();
        return result;
    }

  3. Next we need to assemble the command-line arguments that need to be passed to POV-Ray. These take the form of +[some character][somevalue], for example, +A0.9 sets the anti-aliasing parameter to 0.9 pixels. So we need to iterate the Properties object passed to the constructor and assemble from it a set of command line arguments:

    private String getCmdLineArgs(File includesDir) {
        StringBuffer cmdline = new StringBuffer();
        for (Iterator i=settings.keySet().iterator(); i.hasNext();) {
            String key = (String) i.next();
            String val = settings.getProperty(key);
            cmdline.append ('+');
            cmdline.append (key);
            cmdline.append (val);
            cmdline.append (' ');
        }
        cmdline.append ("+L");
        cmdline.append (includesDir.getPath());
        return cmdline.toString();
    }

  4. Next we need to implement a couple of utility methods that the rendering method will use:

    private File getImagesDir() {
        PovrayProject proj = renderService.getProject();
        FileObject fob = proj.getImagesFolder(true);
        File result = FileUtil.toFile(fob);
        assert result != null && result.exists();
        return result;
    }
    
    private String stripExtension(File f) {
        String sceneName = f.getName();
        int endIndex;
        if ((endIndex = sceneName.lastIndexOf('.')) != -1) {
            sceneName = sceneName.substring(0, endIndex);
        }
        return sceneName;
    }

    Neither is terribly exciting: one gets the images directory from the project as a java.io.File, and the other trims the file extension off a file name (so we can create an image file with the same name as the scene file).

  5. The next method we will add is another utility method. When we render, we will want to show messages on the status bar that describe what is happening: or what went wrong in the event of failure. The UI Utilities API contains a class called StatusDisplayer that lets any code in NetBeans that wants to write to the status bar (the actual implementation of StatusDisplayer is in the windowing system implementations, core/windows in NetBeans CVS).

    Implement the following method, and then add a dependency on the UI Utilities API module from the Povray Projects module:

    private void showMsg (String msg) {
        StatusDisplayer.getDefault().setStatusText(msg);
    }

  6. At this point, we've added a bunch of status messages our code can display, so it is time to add actual text for those messages to the resource bundle. Note that in a number of cases we call:

    NbBundle.getMessage(SomeClass.class, "MSG_Something", someStringArgument);

    ...to fetch a localized string. NbBundle supports embedding arguments inside of a localized string: you can either use the above method, or a variant that takes an array of arguments to embed. So you can define strings in a resource bundle using the syntax

    Could not delete {0} because {1}

    ...and {0} and {1} will be replaced by arguments passed to getMessage().

    This is extremely useful, as often the order in which such strings occur in the result text will be different in different human languages.

    So let's go ahead and add the warning messages we need to Bundle.properties in the same package as PovrayProject:

    MSG_NoMainFile=Main scene file not set for {0}
    MSG_VirtualFile=Not a file on disk: {0}
    MSG_Rendering=Rendering {0}
    MSG_NoPovrayExe=No POV-Ray executable, cannot render
    MSG_NoPovrayInc=No POV-Ray includes dir, cannot render
    MSG_Success=Rendered {0} successfully
    MSG_Failure=Failed to render {0}
    MSG_CantDelete=Could not delete {0}, it is locked or in use

    Now we are almost ready to get down to the nitty-gritty of actually invoking POV-Ray from NetBeans. We will do this in the standard Java way, using Runtime.exec() to start an external process. We also will want to display the text output from the process as it reports its progress, in the Output window. This means we will need a way to write to the Output window. So we will add one more dependency to Povray Projects: add a dependency on the IO API module. To find it, use the class name InputOutput in the Add Dependency dialog:

  7. Handling output from a process is tricky: we will actually have three threads running to handle our process:

    • The thread that invoked the process and is waiting for it to terminate.
    • A thread that is collecting output from the standard output of the POV-Ray process and writing it to the Output window.
    • Another thread that is doing the same thing for the error output of the POV-Ray process.

    So we will need some kind of Runnable which will wait for data from each output stream and route it to the Output window in NetBeans as it becomes available. Writing to the Output window is quite easy: you get an InputOutput object from IOProvider.getDefault() and then write to one of its streams: for example:

        InputOutput io = IOProvider.getDefault().getIO ("Hello", true);
        io.select();
        io.getOut().println ("Hello world");
        io.getErr().println ("This is the standard error output: it should be red");

    ... that's all it takes to make the Output window pop up and display some output!

  8. So before we implement the code that will create the process, lets create the runnable that will wait for output from the process and route it to the Output window: it will be a static nested class inside the Povray class:

        static class OutHandler implements Runnable {
            private Reader out;
            private OutputWriter writer;
            public OutHandler (Reader out, OutputWriter writer) {
                this.out = out;
                this.writer = writer;
            }
    
            public void run() {
                while (true) {
                    try {
                        while (!out.ready()) {
                            try {
                                Thread.currentThread().sleep(200);
                            } catch (InterruptedException e) {
                                close();
                                return;
                            }
                        }
                        if (!readOneBuffer() || Thread.currentThread().isInterrupted()) {
                            close();
                            return;
                        }
                    } catch (IOException ioe) {
                        //Stream already closed, this is fine
                        return;
                    }
                }
            }
    
            private boolean readOneBuffer() throws IOException {
                char[] cbuf = new char[255];
                int read;
                while ((read = out.read(cbuf)) != -1) {
                    writer.write(cbuf, 0, read);
                }
                return read != -1;
            }
    
            private void close() {
                try {
                    out.close();
                } catch (IOException ioe) {
                    Exceptions.printStackTrace(ioe);
                } finally {
                    writer.close();
                }
            }
        }

  9. Now we are ready to implement the render() method in Povray that will invoke POV-Ray. This method should never be invoked from the event thread, because it would block the UI until POV-Ray is finished. So the first thing we do is sanity check what thread we're running on. Then we get the file we need to render, sanity checking that. Then we call getPovray() which may open a file chooser to let the user pick it, and similarly get the default include directory which we will need to pass on the command line.

    Next, we get the directory where we will put the output, assemble our output file name (we use PNG format since NetBeans' Image module supports that). Then we compute the command line that should be passed to POV-Ray. Then we call Runtime.exec() with that argument, wire up the Output window to the output streams from the resulting process, and wait for the process to exit.

    Once it exits, we determine if it succeeded or failed, show an appropriate message, and if it succeeded, return a org.openide.filesystems.FileObject representing the file that was created.

    public FileObject render() throws IOException {
    
        if (EventQueue.isDispatchThread()) {
            throw new IllegalStateException ("Tried to run povray from the event thread");
        }
    
        //Find the scene file pass to POV-Ray as a java.io.File
        File scene;
        try {
            scene = getFileToRender();
        } catch (IOException ioe) {
            showMsg (ioe.getMessage());
            return null;
        }
    
        //Get the POV-Ray executable
        File povray = getPovray();
        if (povray == null) {
            //The user cancelled the file chooser w/o selecting
            showMsg(NbBundle.getMessage(Povray.class, "MSG_NoPovrayExe"));
            return null;
        }
    
        //Get the include dir, if it isn't under povray's home dir
        File includesDir = getStandardIncludeDir(povray);
        if (includesDir == null) {
            //The user cancelled the file chooser w/o selecting
            showMsg (NbBundle.getMessage(Povray.class, "MSG_NoPovrayInc"));
            return null;
        }
    
        //Find the image output directory for the project
        File imagesDir = getImagesDir();
    
        //Assemble and format the line switches for the POV-Ray process based
        //on the contents of the Properties object
        String args = getCmdLineArgs(includesDir);
        String outFileName = stripExtension (scene) + ".png";
    
        //Compute the name of the output image file
        File outFile = new File(imagesDir.getPath() + File.separator + outFileName);
    
        //Delete the image if it exists, so that any current window viewing the file is
        //closed and the file will definitely be re-read when it is re-opened
        if (outFile.exists() && !outFile.delete()) {
            showMsg (NbBundle.getMessage(Povray.class, "LBL_CantDelete", outFile.getName()));
            return null;
        }
    
        //Append the input file and output file arguments to the command line
        String cmdline = povray.getPath() + ' ' + args + " +I" +
                scene.getPath() + " +O" + outFile.getPath();
    
        System.err.println(cmdline);
    
        showMsg (NbBundle.getMessage(Povray.class, "MSG_Rendering", scene.getName()));
        final Process process = Runtime.getRuntime().exec (cmdline);
    
        //Get the standard out of the process
        InputStream out = new BufferedInputStream (process.getInputStream(), 8192);
        //Get the standard in of the process
        InputStream err = new BufferedInputStream (process.getErrorStream(), 8192);
    
        //Create readers for each
        final Reader outReader = new BufferedReader (new InputStreamReader (out));
        final Reader errReader = new BufferedReader (new InputStreamReader (err));
    
        //Get an InputOutput to write to the Output window
        InputOutput io = IOProvider.getDefault().getIO(scene.getName(), false);
    
        //Force it to open the Output window/activate our window
        io.select();
    
        //Print the command line we're calling for debug purposes
        io.getOut().println(cmdline);
    
        //Create runnables to poll each output stream
        OutHandler processSystemOut = new OutHandler (outReader, io.getOut());
        OutHandler processSystemErr = new OutHandler (errReader, io.getErr());
    
        //Get two different threads listening on the output & err
        //using the system-wide thread pool
        RequestProcessor.getDefault().post(processSystemOut);
        RequestProcessor.getDefault().post(processSystemErr);
    
        try {
            //Hang this thread until the process exits
            process.waitFor();
        } catch (InterruptedException ex) {
            Exceptions.printStackTrace(ex);
        }
    
        //Close the Output window's streams (title will become non-bold)
        processSystemOut.close();
        processSystemErr.close();
    
        if (outFile.exists() && process.exitValue() == 0) {
            //Try to find the new image file
            FileObject outFileObject = FileUtil.toFileObject(outFile);
            showMsg (NbBundle.getMessage(Povray.class, "MSG_Success", outFile.getPath()));
            return outFileObject;
        } else {
            showMsg (NbBundle.getMessage(Povray.class, "MSG_Failure", scene.getPath()));
            return null;
        }
    
    }

  10. Next, let's fix our implementation of RendererService to call Povray.render(). Open RendererServiceImpl in the code editor, and modify the render method:

    @Override
    public FileObject render(FileObject scene, Properties renderSettings) {
        Povray pov = new Povray (this, scene, renderSettings);
        try {
            return pov.render();
        } catch (IOException ioe) {
            Exceptions.printStackTrace(ioe);
            return null;
        }
    }

  11. The last step is to open the image when the rendering process is complete. This is quite simple to implement: we just need to look for an OpenCookie on the Node for the image that was rendered. You might be wondering what a Cookie is. A Cookie is the capability that something has to do something. (See http://wiki.netbeans.org/DevFaqLookupCookie and here for details!)

    If you are running a standard configuration of the NetBeans IDE, you already have the Image module installed: it will provide support for opening an image, displaying it in the editor area.

    So, in the Povray File Support module, reimplement RendererAction.RenderWithSettingsAction.run() like this:

    public void run() {
        DataObject ob = node.getDataObject();
        FileObject toRender = ob.getPrimaryFile();
        Properties mySettings = renderer.getRendererSettings(name);
        FileObject image = renderer.render(toRender, mySettings);
        if (image != null) {
            try {
                DataObject dob = DataObject.find (image);
                Node n = dob.getNodeDelegate();
                OpenCookie ck = (OpenCookie) n.getLookup().lookup(OpenCookie.class);
                if (ck != null) {
                    ck.open();
                }
            } catch (DataObjectNotFoundException e) {
                //Should never happen
                Exceptions.printStackTrace(e);
            }
        }
    }

  12. With that, you should be able to clean, build and run the module suite, and be able to run POV-Ray and generate an image in the images/ subdirectory of your project:

    Under Windows, POV-Ray MUST be installed in a directory without spaces AND the project that you create for POV-Ray using the plugin MUST be created in a directory without spaces.


Homework

  • Something you learned this week is how to write to the NetBeans Platform's Output window. Now... try to create a new NetBeans module that will write to the Output window whenever a project has built successfully. Hint: Use the "AntLogger" class from the NetBeans APIs. Don't know how to use it? Use Google and find some examples!

  • Use the resources available in the NetBeans Developer FAQ, the NetBeans Platform Learning Trail, and the NetBeans Blogs to do the following:
    • Write a "Hello World" message to the NetBeans Platform's status bar.
    • Change the title bar of a NetBeans Platform application to "Hello World".
    • Put a "Hello World" JLabel into the NetBeans Platform toolbar.

Next Week

Next week we will cover implementing ViewService and adding actions for that.


return to the topics



Week 8: Implementing ViewService and its Actions

The last piece of our API to implement is ViewService. After we implement it, we begin using our API. To do that, we add a "View" action to our POV-Ray node files. Finally, we add code for determining at runtime whether a file has an image or not, and then add appropriate badges.

Resources

Before continuing with the lesson, watch these two videos. Both cover "porting a Java application to the NetBeans Platform". That is not something that will be directly addressed within this week's lesson, but it is important to be aware of the basic principles relating to porting. Also, these two videos tie together several topics that have been touched on in previous weeks.

Now read through these two documents, which both discuss in some detail the question of porting:

Lab

8.1 ViewService: the Final API Piece

The last piece of our API to implement is ViewService, which will allow us to show the most recently rendered image file associated with a POV-Ray file.

  1. Create a new Java class in org.netbeans.examples.modules.povproject, called "ViewServiceImpl":

  2. We have one utility method we created earlier, for stripping the extension from a file name. We might as well reuse it here, since here we will also need to compute the image name given a scene file. So open the Povray class in the editor, and modify the signature of stripExtension() as follows:

    static String stripExtension(File f) {

  3. Returning to ViewServiceImpl, add implements ViewService to the signature and use the "Implement All Abstract Methods" hint to provide skeleton implementations of all of the methods.

    Now, add the following method to actually find the image file for a given scene file:

    private final PovrayProject proj;
    
    private FileObject getImageFor (FileObject scene) {
        FileObject imagesDir = proj.getImagesFolder(false);
        FileObject result;
        if (imagesDir != null) {
            File sceneFile = FileUtil.toFile (scene);
            if (sceneFile != null) {
                String imageName = Povray.stripExtension(sceneFile) + ".png";
                //Will be null if it doesn't exist:
                result = imagesDir.getFileObject (imageName);
            } else {
                result = null;
            }
        } else {
            //No images dir, there can't be an image
            result = null;
        }
        return result;
    }
            

    ...and run Fix Imports again to import org.openide.filesystems.FileObject.

  4. Implement the constructor and API methods as follows:

    public ViewServiceImpl(PovrayProject proj) {
        this.proj = proj;
    }
    
    public boolean isRendered(FileObject file) {
        return getImageFor (file) != null;
    }
    
    public boolean isUpToDate(FileObject scene) {
        FileObject image = getImageFor (scene);
        boolean result;
        if (image != null) {
            result = scene.lastModified().before(image.lastModified());
        } else {
            result = false;
        }
        return result;
    }
    
    public void view(FileObject scene) {
        FileObject image = getImageFor(scene);
        if (image != null) {
            DataObject dob;
            try {
                dob = DataObject.find(image);
                OpenCookie open = (OpenCookie)
                    dob.getNodeDelegate().getLookup().lookup (OpenCookie.class);
                if (open != null) {
                    open.open();
                    return;
                }
            } catch (DataObjectNotFoundException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
        Toolkit.getDefaultToolkit().beep();
    }
            

    and run Fix Imports to import the necessary classes.

  5. Now we just need to expose our implementation of ViewService via the project's lookup. Modify PovrayProject.getLookup() as follows:

    public Lookup getLookup() {
        if (lkp == null) {
            lkp = Lookups.fixed(new Object[] {
                this,  //project spec requires a project be in its own lookup
                state, //allow outside code to mark the project as needing saving
                new ActionProviderImpl(), //Provides standard actions like Build and Clean
                loadProperties(), //The project properties
                new Info(), //Project information implementation
                logicalView, //Logical view of project implementation
                new RendererServiceImpl(this), //Renderer Service Implementation
                new MainFileProviderImpl(this), //So things can set the main file
                new ViewServiceImpl (this), //Allow things to find/open the image associated with a scene file
            });
        }
        return lkp;
    }

    The trailing comma in the array definition is not strictly necessary, but it's a useful technique for reducing the CVS diff if you're using version control, and so not a bad habit to have: if you add to the array, you only change the lines you added.

8.2 Adding a View action to POV-Ray File Nodes

Now of course, we have implemented the API, but there is no code that uses it. So what we will do here is to add a "View" action to our POV-Ray file nodes.

  1. In the Povray File Support project, open PovRayDataNode in the org.netbeans.examples.modules.povfile package.

    First, we will add one more action into the array of popup menu actions from PovrayDataNode (modified and new lines in blue below):

    public Action[] getActions (boolean popup) {
        Action[] actions = super.getActions(popup);
        RendererService renderer =
            (RendererService)getFromProject (RendererService.class);
        Action[] result;
        if (renderer != null && actions.length > 0) { //should always be > 0
            Action rendererAction = new RendererAction (renderer, this);
            result = new Action[ actions.length + 3 ];
            result[0] = actions[0];
            result[1] = new SetMainFileAction();
            result[2] = rendererAction;
            result[3] = new ViewAction();
            System.arraycopy(actions, 1, result, 4, actions.length-1);
        } else {
            //Isolated file in the favorites window or something
            result = actions;
        }
        return result;
    }

  2. Now we need to implement ViewAction. This can be an inner class inside PovrayDataNode:

    private class ViewAction extends AbstractAction {
    
        ViewAction() {
            putValue (NAME, NbBundle.getMessage(PovrayDataNode.class, "LBL_View"));
        }
    
        public void actionPerformed(ActionEvent actionEvent) {
            ViewService service = (ViewService) getFromProject (ViewService.class);
            FileObject fob = getDataObject().getPrimaryFile();
            service.view(fob);
        }
    
        @Override
        public boolean isEnabled() {
            return getFromProject (ViewService.class) != null;
        }
        
    }

    and run Fix Imports to import ViewService.

  3. Lastly we need to add a localized string, "LBL_View" to the Bundle.properties file in the same package, so that there is some text for the view action's menu item. Add:

    LBL_View=View

  4. At this point, we are ready to run the code. Note that POV-Ray files now have a working View menu item:

8.3 Icon-Badging: Adding File Listening Support

You may have noticed that there are a few methods we are not using on ViewService, which is within the "Povray API" module, particularly isUpToDate(). In the NetBeans IDE, the icon for Java classes has a "badge" in the lower right if the compiled version of it is older than the source file and it probably needs recompilation.

In an ideal world, we would parse POV-Ray source files, find all off their include files, and be able to tell if a rendered image is out of date based on all of that information. However, that would be a bit out of scope for this course, since we have no POV-Ray file parser at the moment. What we can do easily enough, though, is use the implementation we already have of isUpToDate() and mark the PovrayDataNode icon if it is false.

To do this, we will need to add a method to RendererService that lets an object listen for events, which should be fired when the rendered state of a file changes. And this is exactly the sort of case where it is fortunate that RendererService is an abstract class: we can add the methods into the base class, with little risk of breaking any existing code that uses it (in practice there is the remote possibility that some implementation of RendererService already has a final method with the same name and signature [in fact exactly this happened to NetBeans when getCause() was added to Throwable in JDK 1.3], but it is a reasonable change). In this case, of course, we know we are the only ones implementing RendererService, but if this feature were something we were adding after a release, there would be no way to be sure we wouldn't break existing clients by adding abstract methods.

  1. Open RendererService, in the Povray API project's org.netbeans.modules.examples.api.povray package, in the code editor.

    Add the following field and methods. What this will do is let a listener register for change events against a specific scene file, and provide a method that subclasses may call to fire such changes, and two methods that can be overridden to do any additional work needed when a listener is added or disappears.

    Since our PovrayDataNodes are created by the system on demand, they do not have such a well-defined lifecycle. So rather than try to find a point at which we can unregister the listener, we will keep weak references to our listeners, so they can be disposed as need-be:

        private Map scenes2listeners = new HashMap();
        
        public final void addChangeListener (FileObject scene, ChangeListener l) {
            //Get the string name of the scene file: there is no need to hold
            //the FileObject itself in memory forever, we can let it be garbage
            //collected, and just hold the string path, which is less expensive
            String scenePath = scene.getName();
    
            //Make sure what we're doing is thread safe
            synchronized (scenes2listeners) {
                //We will use a weak reference to listeners, rather than have a
                //remove listener method.  This will allow our nodes to be garbage
                //collected if they are hidden
                Reference listenerRef = new WeakReference (l);
                List listeners = (List) scenes2listeners.get (scenePath);
                if (listeners == null) {
                    listeners = new LinkedList();
                    //Map the listener list for this path to the path
                    scenes2listeners.put (scenePath, listeners);
                }
                //Add the weak reference to the list of listeners interested in
                //this scene
                listeners.add (listenerRef);
            }
            //Call our callback method: probably the implementation will start
            //listening to deletions of the image file, because we will need to
            //fire those too.  Do this outside of the synchronized block: never
            //call foreign code under a lock
            listenerAdded (scene, l);
        }
    
        protected void listenerAdded(FileObject scene, ChangeListener l) {
            //do nothing, should be overridden.  Here we should start listening
            //for changes in the image file (particularly deletion)
        }
    
        protected void noLongerListeningTo (FileObject scene) {
            //detach any listeners for image files being created/destroyed here
        }
    
        /**
         * Fire a change event to any listeners that care about changes for the
         * passed scene file.  If the scene file is null, fire changes to all
         * listeners for all files.
         * @param scene a POV-Ray scene or include file
         */
        protected final void fireSceneChange (FileObject scene) {
            String scenePath = scene == null ? null : scene.getName();
            List fireTo = null;
            //Use the 3-state (null, false, true) nature of a Boolean to decide if
            //we have really stopped listening
            Boolean stillListening = null;
            synchronized (scenes2listeners) {
                //Get the list of paths -> weak references -> listeners for this
                //scene
                List listeners;
                if (scenePath != null) {
                    listeners = (List) scenes2listeners.get (scenePath);
                } else {
                    listeners = new ArrayList();
                    for (Iterator i = scenes2listeners.keySet().iterator(); i.hasNext();) {
                        String path = (String) i.next();
                        List curr = (List) scenes2listeners.get(path);
                        if (curr != null) {
                            listeners.addAll(curr);
                        }
                    }
                }
                if (listeners != null && !listeners.isEmpty()) {
                    //Create a list to put the listeners we will fire to into
                    fireTo = new ArrayList(3);
                    for (Iterator i = listeners.iterator(); i.hasNext();) {
                        Reference ref = (Reference) i.next();
                        //Get the next change listener for this path
                        ChangeListener l = (ChangeListener) ref.get();
                        if (l != null) {
                            //Add it to the list if it still exists
                            fireTo.add (l);
                        } else {
                            //If not, remove the dead reference
                            i.remove();
                        }
                    }
                    //If there is nothing listening, remove the empty listener list
                    //and stop paying attention to this path
                    if (listeners.isEmpty()) {
                        scenes2listeners.remove (scenePath);
                        stillListening = Boolean.FALSE;
                    } else {
                        stillListening = Boolean.TRUE;
                    }
                }
            }
    
            //Call the listener removal method outside the synch block.
            //StillListening will be null if we were never listening at all
            if (stillListening != null && Boolean.FALSE.equals(stillListening)) {
                noLongerListeningTo (scene);
            }
            //Again, fire changes outside the synch block since we
            //are calling foreign code
            if (fireTo != null) {
                for (Iterator i = fireTo.iterator(); i.hasNext();) {
                    ChangeListener l = (ChangeListener) i.next();
                    l.stateChanged(new ChangeEvent(this));
                }
            }
        }

  2. Next we need to implement the two protected methods we defined above, in our implementation of RendererService. Open RendererServiceImpl in the code editor.

  3. Now, we will need to implement a listener interface on RendererServiceImpl, in the "Povray Projects" module, so modify its signature as follows:

    final class RendererServiceImpl extends RendererService implements FileChangeListener {
    

    ...and use the editor hint to create skeleton implementations of the methods of these interfaces.

    The thing to note here is that, unlike java.io.File, it is possible to listen for changes on org.openide.filesystems.FileObject, either folders or files.

  4. The API class, RendererService, knows nothing about how image files map to scene files. However, our implementation of it does know how to find the corresponding image file to a scene file. So we will override those methods to listen for changes in the presence, absence, or timestamp of the image file that corresponds to a POV-Ray file. This involves a bit of boilerplate listener code and bookkeeping to decide when to start and stop listening:

        //Keep a list of the paths we are currently listening to
        private Set scenesListenedTo = new HashSet();
        private boolean listeningToImagesFolder = false;
        
        @Override
        protected void listenerAdded(FileObject scene, ChangeListener l) {
            synchronized (this) {
                if (scenesListenedTo.add (scene.getPath())) {
                    if (scenesListenedTo.size() == 1 || !listeningToImagesFolder) {
                        //This is the first call, so we should start listening
                        //on the images folder
                        startListeningToImagesFolder();
                    }
                    listenTo (scene);
                }
            }
        }
    
        @Override
        protected void noLongerListeningTo(FileObject scene) {
            synchronized (this) {
                scenesListenedTo.remove (scene.getPath());
            }
        }
    
        private void startListeningToImagesFolder() {
            FileObject imageFolder = proj.getImagesFolder(false);
            listeningToImagesFolder = imageFolder != null;
            if (listeningToImagesFolder) {
                listenTo (imageFolder);
            }
        }
    
        private void listenTo (FileObject file) {
            //Add ourselves as a weak listener to the file.  This way we can still
            //be garbage collected if the project is closed
            FileChangeListener stub = (FileChangeListener) WeakListeners.create(
                    FileChangeListener.class, this, file);
    
            file.addFileChangeListener(stub);
        }
    
        public void fileFolderCreated(FileEvent fileEvent) {
            //Do nothing
        }
    
        public void fileDataCreated(FileEvent fileEvent) {
            FileObject created = fileEvent.getFile();
            fireSceneChange(created);
        }
    
        public void fileChanged(FileEvent fileEvent) {
            FileObject changed = fileEvent.getFile();
            fireSceneChange(changed);
        }
    
        public void fileDeleted(FileEvent fileEvent) {
            FileObject deleted = fileEvent.getFile();
            fireSceneChange(deleted);
            if (deleted.isFolder() && "images".equals(deleted.getNameExt())) {
                //The images folder was deleted, reset our listening flags
                fireSceneChange(null);
                listeningToImagesFolder = false;
            }
        }
    
        public void fileRenamed(FileRenameEvent fileRenameEvent) {
            //do nothing
        }
    
        public void fileAttributeChanged(FileAttributeEvent fileAttributeEvent) {
            //do nothing
        }

  5. One last change we need to make is to the render() method in the RendererServiceImpl class: it is possible that the images/ directory of the project was simply not there: it can legally be deleted. In that case, there will be nothing to listen to. The first time we render, it will be recreated if necessary. So we need to check if we were listening on the images/ folder, and if not, start now that it's created. So, we need to modify the implementation of render() slightly:

    public FileObject render(FileObject scene, Properties renderSettings) {
        Povray pov = new Povray (this, scene, renderSettings);
        FileObject result;
        try {
            result = pov.render();
            if (!listeningToImagesFolder) {
                startListeningToImagesFolder();
            }
        } catch (IOException ioe) {
            Exceptions.printStackTrace(ioe);
            result = null;
        }
        return result;
    }
        

One thing worth noting is our use of the WeakListeners utility class. This can be used to generate a variant of any event listener which will only reference the actual listener weakly: so you can add a listener to a long-lived object (such as the Project or something held strongly by it), but the listener can still be garbage collected. So, the FileObjects we listen to can outlive the RendererServiceImpl or the Project and not force them to be retained in memory simply because something wanted to listen to changes in a file or folder.

8.4 Icon-Badging: Implementing Icon Badging

Now we need to actually display different icons depending on the rendered state of the scene file being represented. The NetBeans Utilities API offers a handy method for merging multiple images together: ImageUtilities.mergeImages().

  1. Edit the class declaration of PovrayDataNode, in the "Povray File Support" module, so that it implements ChangeListener and add the appropriate stateChanged() method.

  2. Add the highlighted code below to the PovrayDataNode(PovrayDataObject obj, Lookup lookup) constructor, for PovrayDataNode:

    PovrayDataNode(PovrayDataObject obj, Lookup lookup) {
        super(obj, Children.LEAF, lookup);
        RendererService serv = (RendererService) getFromProject(RendererService.class);
        if (serv != null) {
            //Could be an isolated file outside of a project, in which
            //case there is no renderer service
            serv.addChangeListener(obj.getPrimaryFile(), this);
        }
    }

  3. The stateChanged() method can be implemented very simply:

    @Override            
    public void stateChanged(ChangeEvent changeEvent) {
        fireIconChange();
    }

  4. Now we need to override getIcon() to return different icons depending on the state of the Node:

        private static final String NEEDS_RENDER_BADGE_FILE =
            "org/netbeans/examples/modules/povfile/needsRenderBadge.png";
    
        private static final String HAS_IMAGE_BADGE_FILE =
            "org/netbeans/examples/modules/povfile/hasImageBadge.png";
    
        private static final String NO_IMAGE_BADGE_FILE =
            "org/netbeans/examples/modules/povfile/hasNoImageBadge.png";
    
        @Override
        public Image getIcon(int type) {
            Image result = super.getIcon(type);
            ViewService vs = (ViewService) getFromProject (ViewService.class);
            if (vs != null) {
                FileObject file = getFile();
                boolean hasRender = vs.isRendered(file);
                if (hasRender) {
                    Image badge1 = ImageUtilities.loadImage(HAS_IMAGE_BADGE_FILE);
                    result = ImageUtilities.mergeImages(result, badge1, 8, 8);
                    boolean upToDate = vs.isUpToDate(file);
                    if (!upToDate) {
                        Image badge2 = ImageUtilities.loadImage(NEEDS_RENDER_BADGE_FILE);
                        result = ImageUtilities.mergeImages(result, badge2, 8, 0);
                    }
                } else {
                    Image badge3 = ImageUtilities.loadImage(NO_IMAGE_BADGE_FILE);
                    result = ImageUtilities.mergeImages(result, badge3, 8, 8);
                }
            }
            return result;
        }

    Here we have defined a set of constants that are paths to icons, and depending on the state, we will merge various ones with the base. Each of our badge images is 8x8 pixels, so it can neatly be placed in one of the quadrants of our 16x16 icon.

  5. Create the necessary image files in the org.netbeans.examples.modules.povfile package: here are the ones used in this course:

    • hasImageBadge.png :  
    • hasNoImageBadge.png :  
    • needsRenderBadge.png :  

When you run the suite, all POV-Ray files that have been rendered (that is, for which you have created images), will have one icon, files that have changed since being rendered have a different icon, and yet another icon is displayed for files for which no image file has been created:


Homework

  • This week you learned about the NetBeans FileChangeListener class. Go back to the plugin you created in the 1st week, which lets the NetBeans Platform distinguish one kind of XML file from all other kinds. Change that plugin so that a message is printed in the Output window whenever the user changes an XML file of the that kind.

  • You also used the ImageUtilities class some more. Study the Javadoc for that class and try to use some of its other methods! Do you think you can use this class outside the NetBeans Platform? (And what does "outside the NetBeans Platform" mean?) What would you need to do to do so? Experiment and find out for yourself.

  • Watch the two videos at the start of this lesson again. Then port a small Java application to the NetBeans Platform yourself! Ask yourself the question: "How far must an application be ported for it to be truly ported?"

Next Week

We're almost done. Next week we will be adding project build support and putting some finishing touches on our UI and code.


return to the topics



Week 9: Build Support

You may recall that early on, when we were writing PovrayProject, we stubbed out the implementation of PovrayProject.ActionsProviderImpl: so in fact the build, clean and run actions are all disabled when a POV-Ray project is active. We implement them here, together with a "Main File" action.

Resources

Before continuing with the lesson, spend some time exploring the NetBeans Platform Homepage:

http://platform.netbeans.org/

Answer the following 5 questions, based on what you read above:

  1. What is the main difference between Eclipse RCP and NetBeans Platform?

  2. Is it possible to limit the features of the NetBeans Platform Window System? How, exactly? What are some use cases?

  3. Which of the interviewed NetBeans Platform experts was inspired by Dilbert's pointy haired boss?

  4. What's the simplest way of finding out which APIs changed in the last few days?

  5. What's the news item about Maven and the NetBeans Platform announced on 05/23/08?

Don't continue with this course until you've answered the questions above.

Lab

9.1 Project Action Support

You may recall that early on, when we were writing PovrayProject, we stubbed out the implementation of PovrayProject.ActionsProviderImpl: so in fact the build, clean and run actions are all disabled when a POV-Ray project is active. We should quickly implement them:

  1. Open PovrayProject in the code editor, and implement the methods of ActionProviderImpl as follows:

            public String[] getSupportedActions() {
                return new String[] { ActionProvider.COMMAND_BUILD,
                    ActionProvider.COMMAND_CLEAN, ActionProvider.COMMAND_COMPILE_SINGLE };
            }
    
            public void invokeAction(String action, Lookup lookup) throws IllegalArgumentException {
                int idx = Arrays.asList (getSupportedActions()).indexOf (action);
                switch (idx) {
                    case 0 : //build
                        final RendererService ren = (RendererService) getLookup().lookup(RendererService.class);
                        RequestProcessor.getDefault().post (new Runnable() {
                            public void run() {
                                FileObject image = ren.render();
    
                                //If we succeeded, try to open the image
                                if (image != null) {
                                    DataObject dob;
                                    try {
                                        dob = DataObject.find(image);
                                        OpenCookie open = (OpenCookie)
                                            dob.getNodeDelegate().getLookup().lookup(
                                            OpenCookie.class);
                                        if (open != null) {
                                            open.open();
                                        }
                                    } catch (DataObjectNotFoundException ex) {
                                        Exceptions.printStackTrace(ex);
                                    }
                                }
                            }
                        });
                        break;
                    case 1 : //clean
                        FileObject fob = getImagesFolder(false);
                        if (fob != null) {
                            DataFolder fld = DataFolder.findFolder(fob);
                            for (Enumeration en=fld.children(); en.hasMoreElements();) {
                                DataObject ob = (DataObject) en.nextElement();
                                try {
                                    ob.delete();
                                } catch (IOException ioe) {
                                    Exceptions.printStackTrace(ioe);
                                }
                            }
                        }
                        break;
                    case 2 : //compile-single
                        final DataObject ob = (DataObject) lookup.lookup (DataObject.class);
                        if (ob != null) {
                            final RendererService ren1 = (RendererService) getLookup().lookup(RendererService.class);
                            RequestProcessor.getDefault().post (new Runnable() {
                                public void run() {
                                    if (ob.isValid()) { //Could theoretically change before we run
                                        ren1.render(ob.getPrimaryFile());
                                    }
                                }
                            });
                        }
                        break;
                    default :
                        throw new IllegalArgumentException (action);
                }
            }
    
            public boolean isActionEnabled(String action, Lookup lookup) throws IllegalArgumentException {
                int idx = Arrays.asList (getSupportedActions()).indexOf (action);
                boolean result;
                switch (idx) {
                    case 0 : //build
                        result = true;
                        break;
                    case 1 : //clean
                        result = getImagesFolder(false) != null &&
                                getImagesFolder(false).getChildren().length > 0;
                        break;
                    case 2 : //compile-single
                        DataObject ob = (DataObject) lookup.lookup (DataObject.class);
                        if (ob != null) {
                            FileObject file = ob.getPrimaryFile();
                            result = "text/x-povray".equals(file.getMIMEType());
                        } else {
                            result = false;
                        }
                        break;
                    default :
                        result = false;
                }
                return result;
            }

9.2 Ensuring There Is A Main File

We now have handling implemented for all of the standard project actions that make sense for a POV-Ray project. Now, if you've been following very carefully, you may have noticed that there is one bug we need to fix: isActionEnabled() will always return true for build. But we implemented the following code in Povray.getFileToRender():

            render = provider.getMainFile();
            if (render == null) {
                ProjectInformation info = (ProjectInformation)
                        proj.getLookup().lookup(ProjectInformation.class);

                throw new IOException (NbBundle.getMessage(Povray.class,
                        "MSG_NoMainFile", info.getDisplayName()));
            }
    

So if there is no main file set for a project, the build action will be enabled, but if it is invoked, it will throw an exception! The simple choice would be to test if there is a main file, and if not, disable the build action: but this would be rather non-intuitive to the user who might not be able to figure out what is wrong with his or her project. And we would lose an opportunity to explore the Explorer & Property Sheet API, as well as the Dialogs API.

So instead, we will post a dialog which will allow the user to choose which file should be the main file, if none is set when build is called:

  1. First we will set up two more dependencies. Right click the Povray Projects project and select Properties.
  2. On the Libraries page of the Project Properties dialog, click the Add button. Search for the class DialogDisplayer to add a dependency on the Dialogs API.
  3. Click the Add Dependency button again and search for the class ExplorerManager to add a dependency on the Explorer and Property Sheet API.
  4. Right click the org.netbeans.examples.modules.povproject package and chose New > Other > Swing GUI Forms > JPanel Form. Name it "MainFileChooser" The GUI Designer (Matisse) will open.
  5. In the Palette (Ctrl-Shift-8), click the item for JLabel and add a JLabel to the top of the form.
  6. Right-click the JLabel, choose Edit Text, and type "Select Main File".
  7. Drag the right-hand edge of the JLabel to the right edge of the panel.
  8. In the Palette, click the item for JScrollPane and add a JScrollPane to the form below the JLabel.
  9. Drag the bottom right corner of the JScrollPane down and to the right until the bottom and right edge alignment guidelines appear. The result should look like this:

  10. Resize the JPanel, removing the scrollbars, and switch the automatic horizontal and vertical resizing on. The scrollbars can be removed by selecting the JScrollPane and setting both the horizontalScrollBarPolicy and verticalScrollBarPolicy properties to NEVER, in the Properties sheet. Automatic resizing is enabled by clicking the small arrow buttons in the GUI Builder's toolbar.

    Continue tweaking the JPanel to your needs. Once you have deployed the application, the above JPanel should look something like this:

  11. Declare a BeanTreeView and instantiate it:

    BeanTreeView btv = new BeanTreeView();

    The BeanTreeView we are showing is a UI class from the Explorer & Property Sheet API: in fact, it is the very same component that you see in the Projects, Files, Services, and Favorites windows in NetBeans IDE.

    What it will do is, when it is added to a component, search through the hierarchy of parent components until it finds one that implements ExplorerManager.Provider. That component's ExplorerManager will then become the place where the tree view gets its root node to display, and will be what it notifies when the selection changes.

  12. Select the JScrollPane. Right-click it, choose Customize Code, and edit the initial line of code, passing the BeanTreeView to the JScrollPane, as shown below:

  13. Edit the signature of the class so that it implements the interface ExplorerManager.Provider:

    public class MainFileChooser extends javax.swing.JPanel implements ExplorerManager.Provider {

  14. Add the following code to implement ExplorerManager.Provider:

    private final ExplorerManager mgr = new ExplorerManager();
    
    public ExplorerManager getExplorerManager() {
        return mgr;
    }

  15. Modify the constructor so it reads as follows:

        public MainFileChooser(PovrayProject proj) {
            initComponents();
            LogicalViewProvider logicalView = (LogicalViewProvider)
                proj.getLookup().lookup(LogicalViewProvider.class);
    
            Node projectNode = logicalView.createLogicalView();
            mgr.setRootContext (new FilterNode (projectNode,
                    new ProjectFilterChildren (projectNode)));
    
            btv.setPopupAllowed(false);
            btv.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
            //BeanTreeView shows no border by default:
            btv.setBorder (new LineBorder(UIManager.getColor("controlShadow")));
        }

  16. Run Fix Imports to import LogicalViewProvider. It will not find ProjectFilterNode because we have not yet written it.

    Now we need to implement ProjectFilterNode. The Nodes API contains a class, FilterNode, which makes it possible to take one Node, and create another Node which "filters" the original Node: providing different children, actions, properties or whatever it chooses to.

    In our case, we want a FilterNode that will filter out any files that do not have the MIME type text/x-povray - so that, if the user has a text file or an image file or such in their project, they cannot set that to be the main file and try to pass it to POV-Ray.

    We don't actually need to implement FilterNode, we simply need to provide an alternate Children object which filters out files we don't want. Implement this as a nested class inside MainFileChooser:

    private static final class ProjectFilterChildren extends FilterNode.Children {
        ProjectFilterChildren (Node projectNode) {
            super (projectNode);
        }
    
        protected Node[] createNodes(Node object) {
            Node origChild = (Node) object;
            DataObject dob = (DataObject)
                origChild.getLookup().lookup (DataObject.class);
    
            if (dob != null) {
                FileObject fob = dob.getPrimaryFile();
                if ("text/x-povray".equals(fob.getMIMEType())) {
                    return super.createNodes (object);
                } else if (dob instanceof DataFolder) {
                    //Allow child folders of the scenes/ dir
                    return new Node[] {
                        new FilterNode (origChild,
                                new ProjectFilterChildren(origChild))
                    };
                }
            }
            //Don't create any nodes for non-povray files
            return new Node[0];
        }
    }

  17. Now we just need some code to use this panel. That code will go in RendererServiceImpl, before we call Povray.render(). Reimplement the no-argument version of the render() method as follows:

        public FileObject render() {
            MainFileProvider mfp = (MainFileProvider) proj.getLookup().lookup(
                    MainFileProvider.class);
            assert mfp != null;
            if (mfp.getMainFile() == null) {
                showChooseMainFileDlg (mfp);
            }
            if (mfp.getMainFile() != null) {
                return render (null);
            } else {
                return null;
            }
        }

  18. Now we need to implement the method we are calling, showChooseMainFileDlg(). This is the method which will ask the user to pick a main file. It will use the Dialogs API to show a dialog containing an instance of MainFileChooser, and enable the OK button once a file is selected. If the user selects a POV-Ray file, it will be stored in MainFileProvider, and so it will be non-null when we return to the render() method, and so render() will proceed:

        private void showChooseMainFileDlg (final MainFileProvider mfp) {
            final MainFileChooser chooser = new MainFileChooser (proj);
            String title = NbBundle.getMessage(RendererServiceImpl.class,
                    "TTL_ChooseMainFile");
    
            //Create a simple dialog descriptor describing what kind of dialog
            //we want and its title and contents
            final DialogDescriptor desc = new DialogDescriptor (chooser, title);
    
            //The OK button should be disabled initially
            desc.setValid(false);
    
            //Create a property change listener.  It will listen on the selection
            //in our MainFileChooser, and enable the OK button if an appropriate
            //node is selected:
            PropertyChangeListener pcl = new PropertyChangeListener() {
                public void propertyChange (PropertyChangeEvent pce) {
    
                    String propName = pce.getPropertyName();
    
                    if (ExplorerManager.PROP_SELECTED_NODES.equals(propName)) {
                        Node[] n = (Node[]) pce.getNewValue();
    
                        boolean valid = n.length == 1;
    
                        if (valid) {
                            DataObject ob = (DataObject)
                                n[0].getLookup().lookup(DataObject.class);
    
                            valid = ob != null;
    
                            if (valid) {
                                FileObject selectedFile = ob.getPrimaryFile();
                                String mimeType = selectedFile.getMIMEType();
                                valid = "text/x-povray".equals(mimeType);
                            }
                        }
                        desc.setValid(valid);
                    }
                }
            };
            chooser.getExplorerManager().addPropertyChangeListener(pcl);
    
            //Show the dialog: dialogResult will be OK or Cancel
            Object dialogResult = DialogDisplayer.getDefault().notify(desc);
    
            //If the user clicked OK, try to set the main file
            //from the selection 
            if (DialogDescriptor.OK_OPTION.equals(dialogResult)) {
    
                //Get the selected Node
                Node[] n = chooser.getExplorerManager().getSelectedNodes();
    
                //If it's > 1, explorer is broken: we set
                //single selection mode
                assert n.length <= 1;
                DataObject ob = (DataObject) n[0].getLookup().lookup (
                    DataObject.class);
    
                //Get the file from the data object
                FileObject selectedFile = ob.getPrimaryFile();
    
                //And save it as the main file
                mfp.setMainFile(selectedFile);
            }
        }

  19. Lastly we need to make sure that the resource bundle in org.netbeans.examples.modules.povproject contains all of the needed strings: add:

    TTL_ChooseMainFile=No Main File Set in Project
    LBL_ChooseMainFile=Select Main File


Next Week

Next week we will wrap up this course by cleaning up some loose ends.


return to the topics



Week 10: Finishing Touches

We now have POV-Ray support working fairly well: you can create a new project, render it, settings are remembered, images are opened and output goes to the Output window. There are just a few remaining problems: these are details, but small details are sometimes the most important ones:

  • When you right click a project, there are no render or clean actions.
  • Our project templates are mixed in with Java project templates, and don't have human-friendly names ("SamplePovrayProject.zip").

These are things we should take care of now.

Resources

We're now in the final lesson of the course. There are many 'wrap up' tasks that you need to be aware of, beyond those that relate directly to this course. Before continuing with the lesson, read through and understand (and, where applicable, take the steps described) in the following:

Lab

10.1 Improving the Project Popup Menu

The first thing we can do is improve the popup menu for POV-Ray projects: we just need to add a couple of menu items to those already returned.

  1. Right click the Povray Projects project, bring up the Project Properties dialog, and add a dependency on the Actions APIs by searching for NewAction in the Libraries panel. Rather than just using the array of standard folder actions we are getting from the parent folder, we will provide just those actions that we want.
  2. In the Povray Projects project, open PovrayLogicalView in the editor, and find the inner ScenesNode class.

    Override getActions() as follows:

    @Override    
    public Action[] getActions (boolean popup) {
        Action[] result = new Action[] {
            new ProjectAction (ActionProvider.COMMAND_BUILD,
                NbBundle.getMessage(PovrayLogicalView.class, "LBL_Build"),
                project),
            new ProjectAction (ActionProvider.COMMAND_CLEAN,
                NbBundle.getMessage(PovrayLogicalView.class, "LBL_Clean"),
                project),
            new OtherProjectAction (project, false),
            SystemAction.get (NewTemplateAction.class),
            SystemAction.get (FileSystemAction.class),
            new OtherProjectAction(project, true),
        };
        return result;
    }

    This gives us two classes to implement: ProjectAction and OtherProjectAction. The former will simply be an action class which delegates to the action provider of the project, and the latter will use the OpenProjects class from the Project UI API to close the project.

  3. Implement ProjectAction as follows:

    private static class ProjectAction extends AbstractAction {
    
        private final PovrayProject project;
        private final String command;
        
        public ProjectAction (String cmd, String displayName, PovrayProject prj) {
            this.project = prj;
            putValue (NAME, displayName);
            this.command = cmd;
        }
    
        public void actionPerformed(ActionEvent actionEvent) {
            ActionProvider prov = (ActionProvider)
                project.getLookup().lookup(ActionProvider.class);
            prov.invokeAction(command, null);
        }
    
        public boolean isEnabled() {
            ActionProvider prov = (ActionProvider)
                project.getLookup().lookup(ActionProvider.class);
            return prov.isActionEnabled(command, null);
        }
        
    }

  4. Then implement the brilliantly named OtherProjectAction this way, also as a nested class inside PovrayLogicalView. What we're doing here is saving the overhead of one more class to do something simple, and writing one action that either closes the project or makes it the main project, depending on a flag. While not beautiful, it is short enough to be readable: and additional classes to come with a memory penalty, so for trivial things, this approach is not necessarily a bad idea: as long as the result is readable:

    private static class OtherProjectAction extends AbstractAction {
    
        private final PovrayProject project;
        private final boolean isClose;
        
        OtherProjectAction(PovrayProject project, boolean isClose) {
            putValue (NAME, NbBundle.getMessage (PovrayLogicalView.class,
                    isClose ? "LBL_CloseProject" : "LBL_SetMainProject"));
            this.project = project;
            this.isClose = isClose;
        }
    
        public void actionPerformed(ActionEvent actionEvent) {
            if (isClose) {
                OpenProjects.getDefault().close (new Project[] { project });
            } else {
                OpenProjects.getDefault().setMainProject(project);
            }
        }
        
    }

  5. Lastly we need to add the necessary localized strings to the resource bundle for the povray projects package:

    LBL_Build=Render Project
    LBL_Clean=Clean Project
    LBL_CloseProject=Close Project
    LBL_SetMainProject=Set Main Project

And we now have a much improved popup menu:

10.2 Human-Friendly Template Names and Categories

Next, we will make some minor improvements in the way our project templates are presented. They are declared in the layer file for our project, and remain as they were when we created them. Open the layer.xml file for the Povray Project Samples project.
  1. The first thing to change is simply the folder, so that our templates will have their own category. So change the third level of folder name, so that instead of going into Templates/Project/Standard, our project templates will go into Templates/Project/Povray. Copy the SystemFilesystem.localizingBundle entry from the zip file, and paste a copy of it inside the folder tags for the Povray folder. The end result should look like this:

    <folder name="Templates">
        <folder name="Project">
            <folder name="Povray">
                <attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.examples.modules.povproject.templates.Bundle"/>
                <file name="EmptyPovrayProjectProject.zip" url="EmptyPovrayProjectProject.zip">
                    <attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.examples.modules.povproject.templates.Bundle"/>
                    <attr name="instantiatingIterator" methodvalue="org.netbeans.examples.modules.povproject.templates.EmptyPovrayProjectWizardIterator.createIterator"/>
                    <attr name="instantiatingWizardURL" urlvalue="nbresloc:/org/netbeans/examples/modules/povproject/templates/EmptyPovrayProjectDescription.html"/>
                    <attr name="template" boolvalue="true"/>
                </file>
                <file name="SamplePovrayProjectProject.zip" url="SamplePovrayProjectProject.zip">
                    <attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.examples.modules.povproject.templates.Bundle"/>
                    <attr name="instantiatingIterator" methodvalue="org.netbeans.examples.modules.povproject.templates.EmptyPovrayProjectWizardIterator.createIterator"/>
                    <attr name="instantiatingWizardURL" urlvalue="nbresloc:/org/netbeans/examples/modules/povproject/templates/SamplePovrayProjectDescription.html"/>
                    <attr name="template" boolvalue="true"/>
                </file>
            </folder>
        </folder>
    </folder>

  2. The next thing to notice is that, although all of our templates are pointing at org/netbeans/examples/modules/povproject/templates/Bundle.properties, there are actually no localized file names in defined in that file. So one other thing we need to do is define them:

    Templates/Project/Povray=POV-Ray
    Templates/Project/Povray/EmptyPovrayProjectProject.zip=Empty Povray Project
    Templates/Project/Povray/SamplePovrayProjectProject.zip=Sample Povray Project

  3. As a last step, we can edit the two HTML files in org.netbeans.examples.modules.povproject.templates to provide better descriptions.

Homework

You have now extended NetBeans IDE to provide support for POV-Ray. Think back to some of the earlier tutorials you compeleted and... change your project to turn it into a standalone NetBeans Platform application, instead of an extension to NetBeans IDE. Think about these requirements and solve at least 3 of them completely:

  • Testing. Are you going to pay someone to click every piece of UI? Is that a reliable approach? Or are you going to automate the testing process? Investigate functional testing as well as user testing on the NetBeans Platform and apply what you learn to the application you've built in this course.

  • JavaHelp. Your application needs some documentation! What are the advantages/disadvantages of using a completely new module for JavaHelp? When would it make sense to include JavaHelp in the same module as the main source code? If an application contains many modules, is it worth considering putting JavaHelp in many different modules, i.e., in the modules to which particular JavaHelp topics relate? After thinking about these things, add some JavaHelp to your application!

  • Authentication. Not all your modules should be available to all your end users. In addition, you want the users to login to your application. How do you solve these requirements?

  • Title bar. You want the title bar to display the text "POV-Ray Editor". Can you find 3 different ways of doing this? Which approach makes most sense in this particular scenario?

  • Launcher. You want the user to be able to launch the application, of course. Create launchers for each operating system.

  • Welcome Screen. You want to add a welcome screen to your application, displaying a friendly message and some links to documentation. Can you reuse NetBeans IDE's own welcome screen somehow? What exactly would you need to do? You later learn that your users want the welcome screen to be undocked, centered, and non-modal at startup. How/where do you need to tweak the window system to solve this requirement?

  • Tip of the Day. Your users want a "Tip of the Day" feature, one which they themselves can contribute to. Could you use the System Filesystem in some way for this requirement?

  • Licensing. What are the things you need to be aware of in relation to the licensing of your new POV-Ray application?

  • Localization. You have German users as well as English users. What are some approaches to handling this scenario? (Think about modularity.)

  • Modules. You want to keep the download as small as possible. One way of doing so is to ensure that only those NetBeans Platform modules are included that are actually needed.

  • Distribution. You want to distribute your application to your users as a ZIP file. However, you could also distribute it as a JNLP (Java Web Start) application. Try both approaches and think about the differences between them. When would you use the ZIP file approach and when would you use the JNLP approach?

  • Updateability. You want the end user to be able to install extensions to your POV-Ray application. What must you do to provide this support?


return to the topics



Checklist

The course, either directly or indirectly through its accompanying tutorials and other references, has made use of all of the following NetBeans API classes. Are you able to write 1 sentence about each of them? Can you use each of them in your code?

  • FileObject
  • DataObject
  • Node
  • DataNode
  • DataFolder
  • FilterNode
  • TopComponent
  • ExplorerManager
  • Project
  • LogicalViewProvider
  • ProjectInformation
  • ActionProvider
  • OpenCookie
  • WeakListeners
  • IOProvider
  • InputOutput
  • OutputWriter
  • OutputListener
  • StatusDisplayer
  • WindowManager
  • NbBundle
  • FileOwnerQuery
  • FileChangeListener
  • RequestProcessor
  • ImageUtilities
  • FileUtil
  • ProjectState
  • ProjectInformation
  • NotifyDescriptor
  • DialogDisplayer
  • ialogDescriptor

Are there any you can't remember? Don't worry! Look in the NetBeans API Javadoc and in the course notes above. Remind yourself about the class in question. Then make an appointment with yourself, 3 days later, to write 1 sentence about the API class that you forgot. Next, use the class in your code, while making sure that it ends up doing what you're expecting it to do.

Verify your understanding of the above classes by mapping a single API class to each of the statements below:

This NetBeans API class is especially useful when...

  1. ...I want to keep multiple NetBeans Platform views synchronized with each other.

  2. ...I want to localize my application.

  3. ...I want to port a JFrame to the NetBeans Platform.

  4. ...I want to create a generic hierarchy on top of my model.

  5. ...I want to assign the capability of doing something to an object.

In general, when you hear the word "capability", which group of classes should you be thinking of when using the NetBeans Platform?

Finally, look through the list of classes above and identify 3 that you'd like to get to know better. Then use the resources, especially the Javadoc, FAQ, and NetBeans Platform tutorials, to explore your chosen classes. Make sure to also actually use them in a small/large plugin!


return to the topics



Next Steps

Congratulations. If everything worked for you as expected, you will now have completed the main part of the on-line training course. You can continue and become a certified NetBeans Platform engineer or a certified NetBeans Platform committer, by proving your knowledge of NetBeans module development. Do so, by taking one of the following steps:

  • Create a plugin and contribute it to the Plugin Portal.

    When you have done so, write to the course mailing list and someone will verify that the plugin works correctly. You will then receive a certificate stating that you are a NetBeans Platform Certified Engineer.

  • Fix an issue in NetBeans Issuezilla. Search in Issuezilla, after typing "request_for_contribution" in "Status whiteboard":

    You will then have a list of issues that have been specifically marked as being suitable for external contributors to fix:

    If you would like to fix one of these issues, add a comment to it saying that you want to fix it. Then fix it and add the patch to the issue. The owner of the issue will review your code and, if the code is accepted, you will become a certified NetBeans Platform committer.

    Click here to jump to an issue in Issuezilla that will give you a good example: Lorenz Weber, a student from Wuerzburg in Germany, is fixing an issue from Issuezilla and, as you can see, the NetBeans engineers are reviewing his code and giving him helpful feedback.

  • Contribute to the NetBeans Platform Student Exchange.

    Add your idea for a project to the above exchange table. Potentially, someone else somewhere in the world will want to collaborate with you! When you have completed your project, write to the course mailing list and someone will verify that the plugin works correctly. You will then receive a certificate stating that you are a NetBeans Platform Certified Engineer.

  • Write a NetBeans Platform Tutorial or Create a Screencast.

    Go to the List of Desirable Tutorials and pick a topic. Alternatively, pick your own topic, but make sure that it hasn't been covered already. If it's been covered already, maybe you can extend one of the existing tutorials with new information.

    You might also want to create a screencast of a plugin you've created. Or the screencast could be about an interesting area of the NetBeans APIs. For example, maybe you've started using the Visual Library and have some interesting insights to share with others, via a screencast?

In all cases above, you will receive a certificate upon successful completion of the task, if you write to the course mailing list stating you have completed your task, describing what you did, and including your postal address. (Details about the mailing list are here.) Good luck!