Author: Rich Unger This article was submitted as part of the Win With NetBeans contest.
A NetBeans Platform Sample and Tutorial
What does FeedReader do?
FeedReader is a basic RSS/Atom feed browser, modeled after the
Sage plugin for Mozilla
Firefox. It consists of:
A list of feeds (URLs to rss/rdf/atom descriptor files)
A list of headlines from each feed
A browser window (mozilla embedded in a JFrame)
The embedded mozilla frame is provided by the JDIC
library, which uses JNI calls. I have configured this example to use
both the linux and windows versions of the JDIC binaries.
Who Is This Tutorial For?
The primary audience is, of course, people who want to build
applications on the NetBeans platform. I will attempt to fully
document how I created FeedReader, and what each line in the
manifests, layer files, and java source does, especially in cases
where I had to do something ... a little bit quirky.
That is the secondary reason for this tutorial. Quirkiness and odd
workarounds will be called out with a “Quirk Box”:
You have to do something quirky to get this done.
In all such cases, I will link to the appropriate open issue in
IssueZilla. Hopefully, this can be a living document, and these Quirk
Boxes can be eliminated over time, resulting in a platform for which
it is easier to create applications.
For the purposes of this tutorial, I am not going to assume any
particular IDE. In fact, let's just say you've got a text editor,
JDK 1.5.0 (a bug in 1.4.x prevents compilation of this sample, though it will run in 1.4.x),
and Apache Ant 1.6.2 or greater. To simplify my command line
examples, I will assume bash (unix or cygwin). Translate to the
windows command line if you wish. That works fine, too.
I just don't want to mix you up talking about the NetBeans IDE and
the NetBeans platform. Follow this tutorial first. Undertand how
modules work. Then worry about tools (like the NetBeans IDE) to make building them a little
easier.
The first thing you need to get started is a copy of the NetBeans
platform. You don't need to build it from source code. You can just
download a binary
release and unzip it somewhere. I put mine in
/home/rich/netbeans.
Next, it would be helpful to have some sample source code to get
you started. I started with my cluster
build harness. It's convenient because it already has ant build
scripts for building and packaging modules, and it's got a starter
module called “snipe”. Of course, now that I've written this, you
could just as easily start your application with the source tree for
FeedReader.
I unzipped my harness to /home/rich/src/rss.
Finally, you need to configure your build scripts, so they know
where to find your netbeans platform installation:
Modify a few lines in
/home/rich/src/rss/nbbuild/user.build.properties:
By convention, clusters are given version numbers at the end, though
it is not strictly required.
Definition: A cluster is a set of
modules and associated resource files. A netbeans installation can be
made up of a set of clusters, which are selected when you launch
netbeans. The NetBeans IDE, for example, runs the platform4, ide4,
nb4.0, and extra clusters. The idea behind clusters is that you can
have one installation of the NetBeans platform, and many branded
applications sharing the same clusters. I could, for example, install
the rssreader1 cluster into a Netbeans IDE installation, and by
running 2 different launch scripts, I could get the IDE and the
FeedReader, each sharing the platform4 cluster, but otherwise being
completely separate applications.
Next, modify a few lines in
/home/rich/src/rss/nbbuild/user.cluster.properties,
in order to bring the cluster name in synch with what we just
specified in user.build.properties:
Now, I could run ant in
the nbbuild/ directory, and
I'd have a directory /home/rich/netbeans/rssreader1
which contains the snipe module. Of course, I don't want a snipe
module, so I then type ant clean
to get rid of it.
It's time to move on to the modules we do want.
Library Modules
You could bundle the entire FeedReader application into a single
module. It's just not very ... well ... modular. It happens that
FeedReader requires the libraries JDOM, Rome, and JDIC. If you ever
want to extend this application with more modules that may use these
libraries, it would be better to depend on just the library module,
rather than the entire FeedReader. Also, you can make the library
modules autoloading.
Definition: An autoload module is
a module that will be automatically loaded by NetBeans when it is
required (by another module). Until that happens, it won't take up
any memory at runtime.
Adding Modules to the Source Tree
Whenever you add a new module, you need to let the build harness
know about it. Modify two files.
This tells the build scripts which modules are in your cluster, so
they know how to build the cluster as a unit, and in which directory
to install the resulting jar files.
This maps the name of the directory containing the module source
code, with the name netbeans will know it by at runtime (“cnb”
stands for code-name-base).
JDOM
I assume most readers will know what JDOM is. It's an XML parsing
API, and the only reason FeedReader needs it is because the Rome
library uses it.
build.xml: The build file starts by importing another build
file called projectized.xml. Most of the time, that's all you'll need
to have in your module's build file. However, in the JDOM module you
want to include jdom.jar in the module's packaging, in addition to
the module's own jar.
Quirk #1: You shouldn't have to override the build
behavior in order to include additional libraries. You should be able
to declare such dependencies in the project.xml file and the build
scripts should know how to create the appropriate manifest entries
and include them in the nbm file.
So, override 2 targets. The “files-init” target is an exact
copy of the target from projectized.xml, with the addition of the
line:
<include name="${nb.modules.dir}/ext/jdom.jar"/>
The “files-init” target lets the build scripts know which files
belong with the module. These are the files that get deleted when you
run the “clean” target. It is a convention that jars which are not modules go in the ext/ directory.
The other target you need to override is
“netbeans-extra”. This is a hook provided by the build scripts to
give you a place to do things like copy files as part of the
deployment process.
The first line is just the name of the module, and, optionally, a
release-version. Notice that this name matches the <cnb>
(code-name-base) from the modules.xml file.
The second line is the module's
specification-version.
A word on version numbers... A module can
have 3 different version numbers: a release-version, a
specification-version, and an implementation-version.
Let's say, for example, that module A has
release-version 1, specification version 2.0, and
implementation-version beta3. Now, module B is going to declare a
dependency on module A (in its project.xml file). It must
specify a release version of 1. It may
optionally specify a dependency on speficiation version 2.0.
If it does, and the author of A releases a version 2.1, that's okay,
the dependency will still work. The contract is that the public API
classes exposed by module A (see project.xml's <public-packages>
element) will not break compatibility. A dependency on the
specification version only gives B access to those API classes.
If
module B specifies a dependency on implementation-version beta3, it
will only work with that version of module A. However, it will have
access to all the public classes in module A. (If you're getting a
NoClassDefFoundException when you're running your module, it could be
because you're trying to access non-API classes without specifying an
implementation dependency.)
The first line is completely optional. It points to a bundle
containing more manifest entries. These manifest entries are all
localizable strings, such as a display name and description for this
module.
The second line is the standard manifest Class-Path entry, which
puts the jdom.jar file in the classpath of the module's jar file.
Notice that the path “ext/jdom.jar” matches up with where you
copied jdom.jar in the build.xml file.
Quirk #2: Shouldn't have to specify Class-Path. It
should be generated automatically (see Quirk #1).
nbproject/project.xml: This is the file that tells the
build scripts (and the IDE if you're using it) how to generate
dependency delcarations and classpaths. Here, too, you must specify
the familiar <code-name-base> and <path> information.
Next, you must specify any dependencies on other modules. Now, seeing
as though there's no actual code in the module itself, and jdom.jar
doesn't require any other code outside the core JRE classes, there's
really no module dependencies to declare. Still, you should always
declare a dependency on the core OpenAPI classes:
It's kind of a special case. If you don't specify this, with a recent
specification-version, NetBeans will assume this is an old module,
and automatically load a bunch of dependencies at runtime, for
backwards-compatibility purposes.
Next, you need to declare the module's <public-packages>.
This serves two purposes: public packages are viewable to other
modules that declare dependencies on this module, and they constitute
the set of packages that will get javadocs generated when running the
“javadoc” ant target.
The first line makes this module an autoload module. The
second line is appended to the compilation classpath. The third line
is required if you used the <subpackages>
method to declare your <public-packages>
in project.xml.
Quirk #3: Specifying module.javadoc.packages
should not be necessary here. The build scripts should have a sane
backoff, with the assumption that the user isn't interested in
javadocs for this module. Right now, the build fails if this is not
specified.
The Rome library reads
RSS and Atom feeds (with a very simple API, I might add). The Rome
module and the JDOM module differ in only two respects. The Rome
module bundles two jar files (rome-0.4.jar and rome-fetcher-0.4.jar)
instead of one, and project.xml declares a dependency on the JDOM
module:
The JDIC library allows
java programs to take advantage of certain native desktop facilities
such as browsers, mailers, system trays, and MIME type registries.
FeedReader uses the embedded native browser component to render web
pages in a JFrame with the IE or Mozilla rendering engine.
In order to accomplish this, JDIC makes Java Native Interface
(JNI) calls to its shared library (jdic.dll
or libjdic.so), as well as run
native executables (IeEmbed.exe
or mozembed-linux-gtk2). You
need to do a little magic in the module's build file to make these
libraries and executables available to netbeans at runtime.
Notice the set of files declared in the “files-init” task:
Aside from the familiar jar file declaration, there are a list of
native libraries and executables in lib/
and a pair of scripts. The decision to put the native stuff in this
particular directory is somewhat arbitrary, though it is the
suggested location in the architecture
document. The only other thing that needs to know this location
is the startup scripts.
The shell script and batch script are generated by the shellscript
target of the build file. These scripts launch the NetBeans platform
with the rssreader1 cluster and the jdic binaries in their proper
paths.
FeedReader Module
Now that you've got all the library modules you need, you're ready
to make a module that actually does something.
Start by creating the build scripts and
declarative elements of the module. The build.xml
file is blissfully empty, other than the import line for the standard
“projectized.xml”. The nbproject/project.xml
file simply declares dependencies on your 3 library modules, and has
no public packages (this modules provides no API to other modules).
The nbproject/project.properties
file is also empty. The manifest.mf
file should look pretty familiar, too. There's only one new
element there:
Unlike the library modules, you're going to declare things about the
FeedReader's user interface in a layer file, for inclusion in the
“system filesystem.”
Definition: The system filesystem is
where NetBeans stores all the system settings, GUI layout information,
actions, templates, and just about anything else required to maintain the state
of a NetBeans installation. It is comprised of real files located under the "config" directory of
each cluster, the "config" directory of the NetBeans user directory, and virtual "files" declared in module
layer files.
Layer File
The first entry is under the top level folder called Actions.
This is a repository for implementations of javax.swing.Action.
They can be used for menu items, toolbar buttons, and keyboard
shortcuts. Adding this entry will allow users to assign keyboard
shortcuts to the ViewFeedsAction by selecting “Tools ... Keyboard
Shortcuts” from the menu.
The second entry puts the ViewFeedsAction under the “View”
menu. Notice the shadow file syntax, which is like using a soft link
in UNIX, or a shortcut in Windows.
The rest of the entries fall under the Windows2
top level folder. This folder provides the platform with information
about what types of TopComponents
are going to be instantiated, and where to put them. (Incidentally,
this is called “Windows2” because “Windows” is the old,
pre-3.6 window system, which is kept around for backwards
compatibility reasons.) FeedReader contains two TopComponent
definitions: SiteListComponent and EntryListComponent. The latter is
meant to be opened in the default location (the center), so nothing
is necessary in the layer file to override this behavior.
The SiteListComponent,
however, is meant to be docked on the left side, or the “explorer”
mode. So, add an entry:
This declares that a TopComponent
with id “rss_list” will be docked, by default, in the “explorer”
mode. (Incidentally, the extension “wstcref” stands for Window
System Top Component REFerence.) The placement of “rss_list”
within the declared mode is derived from the file feedList.wstcref:
the value of <tc-id id>
must match the basename of the file declared in the layer
(“rss_list”). The same name must also be present in the
Windows2/Components folder
with a “.settings” extension:
The contents of the settings file tells NetBeans how to instantiate
an “rss_list”. You can write the contents of this file by hand,
or you can comment out the entire Windows2 section of this layer
file, start up netbeans, instantiate a SiteListComponent
by running the ViewFeedsAction,
and grab the settings file that gets autogenerated in
$userdir/config/Windows2Local/Component.
Then replace the <serialdata>
section with:
This creates the component using the default constructor of
SiteListComponent. If the
class required a static factory method, you could add a method
attribute:
Now all that's left to do is the actual java code. The first class
declared in the layer file is ViewFeedsAction.java.
This is a simple subclass of CallableSystemAction,
which is a singleton implementation of javax.swing.Action.
public void performAction() {
SiteListComponent.activate();
}
Performing this action will open and give focus to the singleton
instance of SiteListComponent (see implementation of
SiteListComponent below).
public String getName() {
return NbBundle.getMessage(SiteListComponent.class, "SLC_title");
}
The name of the action is stored in Bundle.properties, so it can be
localized.
public HelpCtx getHelpCtx() {
return HelpCtx.DEFAULT_HELP;
}
You would change this if you had a JavaHelp id or a URL with more
specific help for this action.
Quirk #4: Some of the more confusing code in this
class (the stuff dealing with IDs) has to do with ensuring a
singleton instance of SiteListComponent, with the id specified in the
layer file. Ideally, there should be a subclass of TopComponent
called something like SingletonTopComponent, as this seems like a
common thing to want to
do.
/** A hint to the window system for generating a unique id */
private static final String PREFERRED_ID = "rss_list"; // NOI18N
/** The actual id of the (singleton) instance */
private static String s_id = PREFERRED_ID;
...
public static synchronized SiteListComponent getInstance()
{
TopComponent c;
c = WindowManager.getDefault().findTopComponent(s_id);
if (c == null)
{
c = new SiteListComponent();
s_id = WindowManager.getDefault().findTopComponentID(c);
}
return (SiteListComponent)c;
}
...
protected String preferredID() {
return PREFERRED_ID;
}
NetBeans maintains a map of all the TopComponents
currently in memory. The key to this map is what I'm referring to as
the “ID”. the “preferred” ID is just a hint to the window
system to be used when creating a new instance. There's no
guarantee that this hint does anything. It's generally just
useful so you can recognize the name of the component if you're
tracking down problems by perusing the contents of
$userdir/config/Windows2Local.
The static field s_id,
however, is meant to be the ID of the one instance of
SiteListComponent in memory.
It defaults to “rss_list” because that's the value of the ID
given in feedList.wstcref. So,
if this is the first time the user uses this module, the component
with ID “rss_list” will be instantiated declaratively. However,
if the user closes that component, restarts NetBeans, and then
invokes the ViewFeedsAction,
the new component may have a different ID. This is why the
getInstance() method may assign a new value to s_id.
If the ID does change, there's nothing
in the layer file to insist that the new component be docked in the
“explorer” mode. To force this, override the open() method:
private static final String MODE = "explorer"; // NOI18N
public void open()
{
Mode m = WindowManager.getDefault().findMode(MODE);
m.dockInto(this);
super.open();
}
The rest of this class is not NetBeans-specific. The component
consists of a JList and two buttons: one to add a new feed, and one
to delete a selected feed. The list is backed by a SiteListModel.
SiteListModel
The SiteListModel class
adds only serialization to the swing DefaultListModel.
Upon construction, it loads the list of feeds from disk. It updates
the list on disk with each edit of the list. (Overkill? Perhaps, but
it's not going to be a very long list, and once the user sets it up
once, edits will be infrequent.)
The important bit is deciding where to serialize the list.
By putting it in the system filesystem, you can make the serialized
file part of the file structure in $userdir/config:
private static final String DIR = "FeedReader"; //NOI18N
private static final String FILENAME = "feeds.ser"; //NOI18N
private FileObject getSerializedFile(boolean create) throws IOException
{
FileSystem sysFs = Repository.getDefault().getDefaultFileSystem();
FileObject dir = sysFs.findResource(DIR);
if (dir == null)
{
if (create)
dir = sysFs.getRoot().createFolder(DIR);
else
return null;
}
FileObject fo = dir.getFileObject(FILENAME);
if (fo == null)
{
if (create)
fo = dir.createData(FILENAME);
else
return null;
}
return fo;
}
The boolean parameter create
will determine whether you want to create the file if it's not
already there (true for writing, false for reading).
So, the serialized list will end up in
$userdir/config/FeedReader/feeds.ser.
This is a somewhat low-level approach to writing out the list of
feeds. There is, in fact, a NetBeans API that takes care of some of
this for you. You could implement the serialization by subclassing
SystemOption, and just
override readExternal() and
writeExternal(). I chose to
write directly to the system filesystem to give you a better idea of
what's going on under the hood. Also, this implementation gives you
more flexibility to use a different file format. Once you've got the
FileObject returned from
getSerializedFile(), you could
write XML data to it as easily as using ObjectOutputStream.
EntryListComponent
This class defines a component that
will open up in the center, or “editor”, mode, and contain a list
of news items from a single feed.
public class EntryListComponent extends TopComponent
{
protected static final String PREFERRED_ID = "rss_entry_list"; //NOI18N
protected String preferredID() {
return PREFERRED_ID;
}
There's nothing funky going on with the IDs in this class. They're
just acting as hints to the window system.
public static TopComponent getInstance(Feed feed)
{
// look for an open instance containing this feed
Iterator opened = TopComponent.getRegistry().getOpened().iterator();
while (opened.hasNext())
{
Object tc = opened.next();
if (tc instanceof EntryListComponent)
{
EntryListComponent elc = (EntryListComponent)tc;
if (feed.equals(elc.m_feed))
{
elc.initData(feed);
return elc;
}
}
}
// none found, make a new one
return new EntryListComponent(feed);
}
The getInstance() method
ensures that only one instance exists for each unique feed. So, there
may be several tabs open at once, but only one for each feed URL.
Give the feed to the JList,
and set the display name, which will be used on the components tab,
and in window menus, etc.
public int getPersistenceType() {
return PERSISTENCE_NEVER;
}
When shutting down netbeans, don't bother serializing these
components.
EntryList, BrowserFrame, Feed
These classes don't contain much NetBeans-specific code. I'll let
the in-code comments speak for themselves.
Try it Out!
In the nbbuild/ directory,
run ant. Then run
/home/rich/netbeans/rssreader1/bin/rss-reader.sh
(or rss-reader.bat on
Windows). You should now see an empty list on the left, to which you
can add feed URLs.
A working FeedReader! There's just one thing wrong. It still looks
like RSS capability tacked onto NetBeans. There are still toolbar
buttons and menu items that don't do anything useful for RSS reading.
Branding
FeedReader's branding module is a pretty simple module. There's no
java code at all. Just a layer file that hides a bunch of unused
toolbar and menu items:
<folder name="Toolbars">
<folder name="File_hidden"/>
<folder name="Edit_hidden"/>
</folder>
<folder name="Menu">
<folder name="File">
<file name="org-openide-actions-SaveAction.instance_hidden"/>
<file name="org-openide-actions-SaveAllAction.instance_hidden"/>
<file name="org-netbeans-core-actions-RefreshAllFilesystemsAction.instance_hidden"/>
<file name="org-openide-actions-PageSetupAction.instance_hidden"/>
<file name="org-openide-actions-PrintAction.instance_hidden"/>
</folder>
<folder name="Edit_hidden"/>
<folder name="View">
<!-- hide the default web browser that doesn't render stuff very well -->
<file name="org-netbeans-core-actions-HTMLViewAction.instance_hidden"/>
</folder>
<folder name="Window">
<file name="org-netbeans-core-actions-GlobalPropertiesAction.instance_hidden"/>
</folder>
</folder>
The question I'm sure you're thinking is, “How did he know the
names of all those files to hide?” It certainly isn't obvious that,
to hide the “File ... Save” menu item, you need to specify <file
name=”org-openide-actions-SaveAction.instance_hidden”/>.
Well, there's two ways. You can hunt around the netbeans source code,
looking for the layer file in which it was originally declared. Or,
you can use the Bean Browser, a tool included in the “Open APIs
Support” (org-netbeans-modules-apisupport) module on the NetBeans
Update Center. This creates a node in the “Windows...Runtime”
window called “Bean Browser” which lets you browse the system
filesystem.
Very useful little tool.
Aside from the layer file, there is one other unusual aspect to
the branding module. In the build script, you'll see the now-familiar
sections which add resource files to the deployed file set:
<!--
Identifies all the files to be considered part of this module when deployed
-->
<target name="files-init" depends="basic-init">
<patternset id="module.files">
<include name="${module.jar}"/>
<include name="${javahelp.jar}" if="has.javahelp"/>
<include name="${nb.system.dir}/Modules/${code.name.base.dashes}.xml"/>
<!-- additions for FeedReader begin here -->
<include name="${nb.lib.dir}/locale/core_rss.jar"/>
<include name="${nb.modules.dir}/locale/org-netbeans-core-windows_rss.jar"/>
</patternset>
</target>
<!--
netbeans-extra is a hook provided to plug in file copying.
-->
<target name="netbeans-extra" depends="init">
<mkdir dir="${netbeans.dest.dir}/${cluster.dir}/${nb.lib.dir}/locale"/>
<mkdir dir="${netbeans.dest.dir}/${cluster.dir}/${nb.modules.dir}/locale"/>
<jar destfile="${netbeans.dest.dir}/${cluster.dir}/${nb.lib.dir}/locale/core_rss.jar"
basedir="core"/>
<jar destfile="${netbeans.dest.dir}/${cluster.dir}/${nb.modules.dir}/locale/org-netbeans-core-windows_rss.jar"
basedir="core-windows"/>
</target>
In this case, you're adding two jar files: core_rss.jar
and org-netbeans-core-windows_rss.jar.
These jars correspond to core.jar
and org-netbeans-core-windows.jar.
In fact, any jar in the standard netbeans install can be “branded”
by placing another jar relative to the original:
Recall that the shell script in the JDIC module passes “–branding
rss” as a parameter. So the “brandname” in this case is “rss”.
Just about any resource file can be branded. Icons, bundles, layer
files, etc. In core_rss.jar,
I've branded the splash screen and a resource bundle, which contains
keys that affect how the splash screen is displayed. In
org-netbeans-core-windows_rss.jar,
I've branded just one bundle, to override the main title bar's text.
Try it Again!
Now you should see a screen like the screenshot at the beginning
of this article. And now you know how to write a real application for
the NetBeans platform.