Documenting Projects with Apache Forrest
by Kyle Downey05/26/2004
One of the things that divides good from great software projects is clear, effective documentation. It's not easy to maintain, though, and even if you're determined to produce good documentation, you may find yourself bogged down in the details of formatting and publishing. Apache's Forrest lets you focus exclusively on your content, and automates the rest: nothing more, nothing less. Whether you want to document an open source project or an internal application, Forrest can help you build professional, polished documentation.
This article will give you a very basic introduction to Forrest -- just enough to get you started and give you a taste of what's possible -- to see if Forrest's for you; you can find out much more from Forrest's own documentation. We'll close out by taking a look at a small extension to Forrest to give you a sense of what it takes to customize this documentation platform using Java.
Introduction to Forrest
Forrest takes raw documentation content (usually in a simplified XML authoring format) and automagically converts it into a web site with:
- Smart hierarchical menus
- Named, logical links, making it easy to cross-reference without regard to the physical location of files
- Custom headers and footers
- "Breadcrumbs" links around the site
- Multiple tabs
- "Favorites" icon for IE and other supporting browsers
- A choice of "skins" with different appearances
- Conversion of SVG to a variety of image formats (e.g., PNG)
- A choice of dynamic generation (run it as a web app) or static generation (run it by hand and generate HTML)
- Links to PDF versions of pages, or even the whole site
The Forrest web site itself is a large application of Forrest, so you can get an idea of what's possible by browsing around the site.
Internally, Forrest is a sophisticated application of Cocoon. It's an XML application framework that can be used for offline web site generation and dynamic web site generation, but really it should be thought of as an XML processing pipeline. You can plug in generators (XML sources), transformers (XML-to-XML translations) and serializers (XML to other formats, such as PDF). Cocoon provides all kinds of built-in generators, transformers and serializers -- many of which Forrest uses -- but more importantly, it orchestrates them. You use XML files to define your pipeline and then execute the Cocoon engine to run it.
|
Related Reading
XML Hacks |
Personally I think Forrest's great strength is the strict separation between content and view. Too often, when writing documentation in straight HTML, I found myself hung up on the details of look and feel, and maintenance was a nightmare. Changes to the overall appearance require altering all of the documentation pages. With Forrest, it's very easy to target the content change or skin/appearance configuration you want to make and leave it to the tool to reflect it throughout the site.
Forrest-Enabling a Project: A Quick Start
Forrest should win a prize for quickest and most convenient start; this is an area where many open
source projects fall down. You can get from nowhere to a project with
skeletal documentation with a single command. Once you've unpacked the Forrest binary distribution,
all you need to do is set the environment variable FORREST_HOME and add
${FORREST_HOME}/bin to your PATH. From there, just run forrest seed
in the top level directory of your project.
This will execute Ant behind the scenes, using a custom build file.
This whole process will give you a default forrest.properties file in your project root directory and a src/documentation/content/xdocs directory with sample documentation in the various formats Forrest supports. It also sets up a default skin configuration in src/documentation/skinconf.xml.
From here, you can run forrest any time to re-generate your web site. It will show up under
build/site and you can either open the local files in your web browser or take advantage
of Forrest's online mode.
Customizing the Xdocs
The heart of the documentation is in src/documentation/xdocs. This is the raw content that Forrest will transform into HTML and PDF. The "seed" target we ran for setup copied in a whole bunch of sample files that you can browse through. We can delete most of them, leaving index.xml, site.xml, and tabs.xml.
The site.xml and tabs.xml files control navigation. I modified the
ones in the downloadable source accompanying the article (see "Resources" below) to have one tab with links to this
article and to a new XML file, example.xml, which demos the plugin we'll develop later.
Note that there's no DTD: you can have arbitrary XML under <site>
that reflects your navigational structure.
In this case we're creating one block, about, that we'll name in tabs.xml to use on our single tab. This free-form XML structure creates a hierarchical table of contents for the site. Nesting indicates different menu levels, and whole blocks can be associated with a tab, as previously mentioned. Drilling down, the article and example elements link to the two files I added to what forrest seed created for us. Referencing these elsewhere will be as simple as <link href="site:article"> and <link href="site:example">; even if filenames change, your navigational links can stay the same.
<about label="About">
<index label="Index" href="index.html"
description="Intro to Forrest"/>
<article label="The Article" href="article.html"
description="Article on Forrest"/>
<example label="Code example: API docs"
href="example.html"
description="Extension for Forrest"/>
<changes label="Changes" href="changes.html"
description="History of Changes"/>
<todo label="Todo" href="todo.html"
description="Todo List"/>
</about>
Take a look at example.xml to see the XML structure for content. It looks a lot like
HTML, but with all of the style-related code taken out. You can use elements such as <warning>
and <section> to structure the content without declaring how it will be displayed. If you
know HTML, you can learn Forrest's subset in an hour; it's very natural.
Re-Skinning the Documentation
The default skin is quite Apache-specific. The favicon.ico is an Apache feather icon,
the color scheme matches Apache's web site, etc. I prefer the Krysalis skin, and with Forrest the change is easy. Just open up forrest.properties and uncomment
project.skin=krysalis-site, then re-run Forrest. You can see an example of the Krysalis
look for Forrest documentation here.
Getting Down to Code: Adding the api Link Type
Forrest supports two link types: site and ext. Site links, such as site:about, get mapped to
XML elements in site.xml, under <site>. External links, such as ext:forrest,
get mapped to XML elements in the same file, under <external-refs>. This lets you change
your site structure and sites you link to outside without altering your documentation content
-- another example of our theme of separating view from documentation.
One link any Java developer would want is to Javadocs. Given a class name, it would be nice to have
Forrest generate an HTML link to its Javadoc; e.g., api:foo.Bar might become apidocs/foo/Bar.html
when translated. We're going to add an extension that will do just this, using the same mechanism that
makes site and ext work.
Cocoon uses the concept of an "input module" in lots of different places. It's essentially
a component representing a universe of name-value pairs. Different implementations can
interpret different kinds of names. At the top level, we want a name such as example/foo.Bar
to translate to a path to a local Javadoc. To get at this, we need an additional level of
indirection, to translate example to a base path, to which
our root module can then append a path to an actual Javadoc HTML file. This lets us have
more than one set of Javadocs, and make their locations configurable. We might have one
set of Javadocs called example1 and another called example2, for instance. In the code
and the XML, I'll refer to this name as a set ID.
This is where the reuse and nesting of InputModules starts to come in handy. Cocoon already
has a module that can read from a file and interpret the incoming names as XPaths, and
return the value of an XPath as the value. We'll use this to read the
site.xml file that Forrest uses to map virtual to actual links. Another chainable module knows how
to add a prefix and a suffix to the name requested. We use this to prefix our XPath with
/site/apidocs and append /@base. What goes in the middle is the
name requested by our custom module, ApidocInputModule. We'll pass in the set
ID here, and get back an XPath that looks like /site/apidocs/example/@base.
With a little string manipulation, we can convert the class name to a path to HTML, append
it to the base we got back, and voilà: a full path to a Javadoc given a class name.
That's a good deal of hand-waving, but because we're taking advantage of Cocoon's rich
set of features to do this with just a few lines of code, I wanted to go over what it
does first. First we want to implement InputModule. Because we'll be chaining together other InputModules, we'll subclass a convenience base
class for this purpose, AbstractMetaModule. Our primary method is getAttribute(), which maps a name to a value.
The first order of business is to configure the module and separate out the Javadoc set name from the class name.
public Object getAttribute(String name,
Configuration configuration,
Map objectModel)
throws ConfigurationException {
// meta module doesn't set up the child
// InputModule until asked
lazy_initialize();
// Cocoon lets you supply both a default
// configuration and a runtime configuration;
// we need to pick up the latter
Configuration inputConfig = null;
String inputName = null;
if (modeConf != null) {
inputName = modeConf.
getChild("input-module").
getAttribute("name", null);
if (inputName != null) {
inputConfig = modeConf.
getChild("input-module");
}
}
// the first part of the name is the set;
// if none, it becomes "default"--the rest
// is the class name and optionally the
// method name
String setName = null;
int ndxSlash = name.lastIndexOf('/');
if (ndxSlash == -1) {
setName = "default";
} else {
setName = name.substring(0, ndxSlash);
}
....
}
First, a note on the extra parameters to the configure() method, which we'll (mostly)
ignore for this plugin. Configuration provides access to the runtime parameters passed to the module, and the objectModel parameter provides access to the Cocoon environment. I said "mostly" because in an earlier draft of
the code, I found the plugin wasn't working because it hadn't yet been configured and initialized.
We'll do this by calling the lazy init method, as shown, making sure the runtime overrides
for configuration get used (if any; Cocoon lets us configure at different levels), and
finally overriding configure() to read that nested <input-module>
that I initially neglected:
public void configure(Configuration config)
throws ConfigurationException {
super.configure(config);
this.inputConf = config.
getChild("input-module");
this.defaultInput = this.inputConf.
getAttribute("name", this.defaultInput);
}
Pages: 1, 2 |


