Tapestry Training -- From The Source

Let me help you get your team up to speed in Tapestry ... fast. Visit howardlewisship.com for details on training, mentoring and support!

Saturday, August 28, 2004

ScrapeSDL -- Convert SDL back to XML

With the release of HiveMind 1.0-rc-1, I've managed to "strand" a number of HiveMind users who had adopted SDL (Simple Data Language). SDL is no longer supported.

ScapeSDL.zip is an Eclipse project containing code that can convert an SDL file into an XML file. It's what I used to convert all the SDL in HiveMind itself back into XML (though I did have to do a bit of manual reformatting).

Tapestry and HiveMind at ApacheCon

Last year, I did a short, 60 minute, session on Tapestry at ApacheCon ... and I really think that launched things for me. I got a lot of good feedback on Tapestry and on my speaking style, and its opened a number of doors for me.

This year at ApacheCon, I'll be doing a long session on Tapestry on Sunday that will cover most of the material in my two normal Tapestry sessions (form building and component building). Additionally, I'll have a HiveMind session late on Wednesday. Details may someday be posted on the ApacheCon schedule.

Erik Hatcher, Matt Raible, and many others will be there. It was a good time last year (of course, I did bring my wife, Suzanne).

I'm spending too much time in Vegas, for someone who doesn't gamble. I was there in April for TheServerSide Symposium, I'll be there in October for a friend's birthday, and back in November for ApacheCon. Thankfully, I have my rolling laptop case (what a difference that makes when your laptop weighs in at about 15 pounds!).

Wednesday, August 25, 2004

HiveMind 1.0-rc-1

Following a successful vote (notable only for the absence of a vote from Knut), I'm building out HiveMind 1.0-rc-1 at this very moment. That's Release Candidate 1 ... and I expect to be rolling to a final 1.0 release very quickly.

There are a boat-load of plans for 1.1 and I hope that, unlike Tapestry's lethargic pace, we can get a 1.1 out in just a matter of months or even weeks.

"Invite 6 friends to GMail" -- It's open season!

I've built up six GMail invites ... drop me an email at hlship AT gmail DOT com if you'd like one. First come, first served.

Update: Sorry, all gone!

Monday, August 16, 2004

Dependency Injection -- the mirror of Garbage Collection

A ways back, the idea of a garbage collected language was tantamount to heresy. "It'll be slow." "It isn't necessary." "How hard is malloc()?" "Reference counting is all you need." etc.

Nowadays, garbage collection is the norm and there's no going back. By freeing us of concerns about allocating and freeing memory, new development techniques became possible. For me, personally, it was very liberating ... I've always coded in terms of small modules working together. Fifteen years ago I was writing almost-object-oriented code in a heavy duty procedural language (PL/1) ... it was just much, much harder.

Garbage collection liberates me to solve problems in my way ... using lots of small, well understood objects working together. When you have to be concerned about every object creation and destruction, you get miserly about them and before you know it, you have these big, monolithic objects that break all the now accepted rules about separation of concerns. You saw this inside the otherwise stellar NeXTSTEP libraries ... there were quite a few kitchen-sink objects and your main extension point was to subclass an existing class.

I used to be dismayed that the UML sequence diagrams did a really poor job of illustrating inheritance. It was really hard to properly diagram an object invoking a method in itself that's implemented in a super-class. Nowadays, I realize that the Amigos were ahead of the curve: aggregation trumps inheritance. And don't think I haven't been thinking about this in terms of Tapestry, which mandates inheritance (you must subclass AbstractComponent, BaseComponent or BasePage). I have been, and some future release of Tapestry will break that requirement.

But the story doesn't end with garbage collection (even if the individual objects do). Garbage collection is the last stage of an object's life cycle, but there's just as much going on at the start of the object's life cycle. That's why component frameworks and dependency injection containers (such as HiveMind, Spring, Picocontainer and Avalon) are so important.

"Why do we need an external descriptor?" "It just isn't necessary." "How hard is it to new Foo()?" Sound familiar?

Once you start thinking in terms of large numbers of objects, and a whole lot of just-in-time object creation and configuration, the question of how to create a new object doesn't change (that's what new is for) ... but the questions when and who become difficult to tackle. Especially when the when is very dynamic, due to just-in-time instantiation, and the who is unknown, because there are so many places a particular object may be used.

HiveMind didn't spring up out of thin air, it was based on me seeing an awful lot of code, production code, that was either a) inefficient, b) unnecessary or c) incorrect (often due to errors related to class loaders and multi threading). Too often it has been option d) all of the above.

Dependency injection acknowledges an initial state for objects ... the construction state, where objects are created and configured before being put into production. With HiveMind, you get the benefits of simplicity: your services are fundamentally POJOs. But you also get all the life cycle benefits: just-in-time, thread safe construction. Any reasonably sized system is going to need those benefits ... they are a cornerstone of providing the efficiency and robustness your clients and customers require. If you don't get them from a container such as HiveMind, you'll be writing that code yourself. Again and again, introducing bugs all the way.

You're willing to let something else manage the death of your objects because it, the Garbage Collector, can do a better job than you can. Likewise, you should accept that a container, whose only responsibility is to construct and configure your objects, will do a better job of it than your own code. Embrace that fact ... and get back to interesting work!

Monday, August 09, 2004

Groovy and Tapestry: Followup

As a follow up to my earlier post about combining Tapestry and Groovy, the folks involved (Michael Henderson and Richard Hensley) have combined their implementations and set up a SourceForge project.

It looks pretty sweet; you create a script and static methods in the script can be listener methods. Also, by using the correct method names (say, pageBeginRender), you script's static methods will be invoked at the right times. Also, static listener methods.

I think Hani had a diatribe not long about about flexibility. However, one of the core features of Tapestry is how it is divided into subsystems, precisely to provide this kind of flexibility ... such as changing the very nature of the framework! For the vast majority of users, that flexibility is masked as unnecessary complexity (which is probably the root of Hani's blog-arrhea). But often, that complexity is a reflection of a far reaching vision. Tapestry has always supported the notion that some pages or components would not be files in the WAR, but would be located from an external source, or even created dynamically at runtime. The bridge code that allows Groovy scripts to act like pages is one offshoot of that vision.

Friday, August 06, 2004

Atlanta Java User's Group Appearance

I'll be demonstrating Tapestry and Spindle on Aug. 17th at the Atlanta Java User's Group. Thanks to Jay Zimmerman of No Fluff Just Stuff for setting it up!

HiveMind happenings

So, by developer vote, SDL (Simple Data Language) has been removed, and the ocean may return to a gentle simmer. I learned a bit about JavaCC with SDL and was excited by it, but I'd rather concentrate on what HiveMind does really well, and not be sidetracked. Instead, we're looking for ways to further reduce the size and complexity of HiveMind's XML module deployment descriptors, and reduce the amount of code in general.

One thing else I've been working on is the ServicePropertyFactory. The idea here is that sometimes service A needs a property from service B to do its work. One approach would be to use service-property:B:foo to inject property foo from service B into service A.

But what if the value can change over time? In Tapestry, examples of this will be access to the HttpServletRequest, HttpServletResponse and HttpSession objects for the current user and current thread. These objects are specific to one thread and change on every request.

Your code could hold a reference to service B and get the property from B every time it is needed. But that's more code, more code paths, more Law of Demeter violations.

What would be nice would be if that property of B could be bound to service A? That's what ServicePropertyFactory does. It creates a service implementation, a proxy. The proxy keeps a reference to service B and, on each method invocation, gets the property from B and re-invokes the method on the property. This is service C, the binding between A and B, and is what gets injected into A.

This allows the code for A to treat a property (say, the HttpServletRequest) as if it was unchanging, when in fact, it is constantly changing. There are pitfalls ... the cost of accessing the methods of the proxy is higher than an ordinary object (it has to go get the real value from the underlying service), and you wouldn't want service A to pull any information out of the property and store it, since that kind of data will likely not be valid after the current request.

What this does do is allow the more dynamic parts of the HiveMind application to be isolated in their own services and allows the rest of the code to stay simple.

Friday, July 30, 2004

Ant best practice: One Compile, One Directory, One JAR

Do yourself a favor. When you are laying out your source tree, follow this rule: One Compile, One Directory, One JAR. Just because Ant supports includes and excludes on its <javac> and <jar> tasks doesn't mean you should actually use them!

A proper source tree structure may have multiple source code roots, and will run javac multiple times -- but each execution will compile all files under a particular source directory into its own, individual classes directory. The contents of that directory (perhaps combined with some deployment descriptors or other resources) will form a JAR. If anything depends on that code, it should add the JAR (not the classes directory, and certainly not the source directory) to its compile time classpath.

Why is the compile-and-split approach dangerous? It makes the build process and build scripts more complicated -- possibly lethally so. It ensures that the build process will not generate class files the same way (and into the same directories) as the IDE. It increases the likelyhood that some classes will be duplicated (compiled multiple times to multiple directories and packaged into multiple JARs), which can easily lead to mysterious ClassCastExceptions at runtime.

A particularly important case involves deployment into an application server. Once you get into complex EAR deployments (with multiple EJB JARs and WARs), the class loader issues can quickly get out of control. By using multiple compiles, you can cleanly ensure that the libraries available at build time match the libraries available to the runtime classloader for that artifact (that EJB JAR or WAR). Without this simple bit of discipline and organization, you can lose big chunks of time to tracking down ClassNotFoundExceptions.

I'm always amazed when I see this kind of thing in otherwise sane code and build environments. I've also seen just how far wrong this approach can go ... such as one source tree, one compile, 40+ JAR files, 10,000+ lines of build scripts!!

.Net vs. Struts ... and the winner is Tapestry!

Just something I noticed on the Cardsharp on Software blog:

Tapestry from the Jakarta Project. Yes, it's not JSF. Yes it's "non-standard." Yes, it requires you to learn a new library and a new way of doing things. But it's better. Far less configuration than Struts, and what is there makes sense. Instead of a jumble of disconnected concepts, the whole library is built around an event-driven mental model that just makes sense. Things that would take me 5 lines of code in Struts take one or two in Tapestry. Imagine being able to tie an event handler in a Java class to a link or a button with no configuration file changes! Think about forwarding to a new page and passing objects to the "action" by setting properties on it istead of hucking everthing into the request or session. Think about how much you like Tiles and imagine what it would be like if everything was a tile, but didn't require all the verbose <tiles:put> lines just to set parameters on your tiles. Think about all those times you'd wished you could put custom configuration information into your <ACTION> tags in the struts-config.xml (especially if you've tried to customize it using the <set-property> nonsense!). Think about how nice your life would be if your ActionForm and your Action were actually in the same class in a way that made sense, and think about how nice it'd be if the framework implemented all the getters and setters for you. Think about implementing complex interactions without jumping through all kinds of hoops to manage the state of your page.

And I'm only scratching the surface here.

Thursday, July 29, 2004

This Great Hacker Does Use Java

Contrary to Paul Graham's misguided treatise on Great Hackers, this great hacker (by his definition, I prefer the term "developer") absolutely does use Java and loves it. You can pry it out of my cold, dead hands kind of love.

Side note: coming from an Objective-C background, I was initially very dismissive myself of Java. I still prefer many aspects of the Objective-C syntax, and Objective-C has killer runtime libraries ... but I've grown to love and be reliant upon Java type safety, and garbage collection. Both are pre-requisites for solving problems in an object oriented way: using large numbers of small, simple objects.

I feel there is a kind of insipid game out there I call "chase the gigahertz". Every time the language (COBOL, then C, then C++, then Java, now Ruby and friends) catches up to the available computing power and the available libraries are in a state where you can actually get something accomplished, there's a lemming-like urge to jump ship and start all over again in the latest and greatest ... with a less efficient, less optimized environment that pushes the performance barrier back up and out of reach.

Wait. Stop. Ask yourself "why?". As the differences between generations of languages become more and more subtle, you have to ask yourself ... what do I gain by switching? What cost do I pay? Am I throwing out the Baby with the Bathwater? Sure, you'll be incrementally more effective at doing the kind of things that look good in demos and tutorials ... but the moment you reach for your handy XML parser and it isn't there, or a friendly GUI toolkit and you have to start from scratch. Database access. Command line parsing. Transaction management. Yes, analogues exist for all of these, but you'll be starting from scratch to learn their APIS, quirks, bugs and workarounds. Just how compelling is that nifty bit of syntax, those couple of characters you don't have to type, that convenient built-in function?

For me, Java hits a terrific sweet spot. Vast numbers of open-source libraries. Very competent language. Minimal amounts of stupid syntax. Of course, there are things I'd change ... but in my analysis, its not about the language syntax its about how you use the language. I'm a framework guy; when I get annoyed at writing dumb, repetitive code I build a framework to take care of that dumb, repetitive stuff for me. I write it once, test it, forget how it works. Tapestry saves me a lot of trouble when dealing with the ugly intricacies of web applications. HiveMind saves me a lot of trouble when building complex apps from lots of simple little pieces. I don't throw out years of effort and hope some new language will solve my problems for me ... I face up to my issues and fix them right here, right now, in Java and be done with it.

Java is good enough. Sometimes "good enough" today is better than "ideal" tomorrow. I can get insanely great things done in Java today, and tomorrow, and the day after.

I'm not saying you should be complacent. Dave Thomas recommends learning a new language at least every year, to give you more experience, more insight into new ways of accomplishing things in your language of choice. I hope the Java language designers to the same, and give us some stuff we could use (such as continuations and closures). As a software craftsman, your technique should transcend any one programming language.

But back to Paul Graham; he's making that tired claim about productivity, as if productivity is linked to characters typed. So typing productivity trumps everything else? As you can see, I don't think so. I think he's fallen prey to a bad analogy ... that because some great hackers chose to use scripting languages such as Perl, Python and Ruby then all productive developers should use those languages for all chores, great and small. He's decided that one language (or class of language) is universally correct, and he's using that as a litmus test for being a great hacker.

Whereas, I think "the appropriate tool for the job", scripting languages for simple, scripting things. Power languages, such as Java, for long term, high quality, maintainable development. Individuals can move mountains, if they have the right tools. For the applications and frameworks I build, the right tools are Java, the Java runtime and the innumerable Java libraries.

German Tapestry Book Update

As mentioned previously, a German language book on Tapestry has been published ... and I now have a copy of it myself (thanks, Stefan!). Sorry, no review ... I don't read German, which means I can't even read my own vorwort (forward).

Saturday, July 24, 2004

Copland: HiveMind for Ruby

From the department of nifty!: Copland (named after the composer) is a Ruby version of HiveMind. Not just similar ... expressly based on HiveMind.

Wednesday, July 21, 2004

Groovestry?

Michael Henderson has been combining Groovy with Tapestry and has some pretty exciting results. He's not satisified with just listener methods in Groovy (using the little known <listener-binding> element) ... he wants page classes in Groovy, and eventually, he wants to eliminate page and component specifications and do those in Groovy as well. I can't think of a reason why not!

Tapestry Goes German

If you read German, you have something new to look forward to: Stefan Edlich and Patrik Kunert have written their own Tapestry book, subtitled Webanwendungen mit dem Apache Framework. A few more details are available at Software & Support Publishers' web site (which includes a downloadable chapter). Stefan emailed me to share his joy at holding a copy in his own hands, something I can relate to (was that only back in March?).

Sunday, July 18, 2004

HiveMind and autowiring

Sometimes the function of a community is to force you in the right direction. With HiveMind, there's the ongoing concern of too-much XML. Erik Hatcher somewhat forced the issue (he can be very incisive) and the response was the controversial Simple Data Language.

However, even that may not have been enough. The SDL is more concise, but your still have a lot to say in it. Over time, the BuilderFactory has been extended, in a number of ways to decrease the amount of SDL you need to put in place. For example, if a service needs to know its service id, it just needs a setServiceId(String) setter method and BuilderFactory will invoke it.

My more recent changes build on a very nice patch supplied by Marcus Brito, which allows you to access a service within the Registry using just its service interface. HiveMind will find the lone service point that implements that service and return it (throwing an exception if no service, or more than one service, implements the interface). Marcus did a good job, supplying a nice patch including JUnit tests (I only needed to do some minor touch ups). I can see how useful the approach is, though there's the danger of it causing problems (what happens at a later date when a second service is introduced?).

I've introduced another form of autowiring into BuilderFactory: after it has set all the properties it knows about (via autowiring and expicit sets) it checks the remaining writable properties. Any such properties that are interfaces are assumed to be services, and the unique service for that interface is located. Again, errors abound when the interface isn't a service, or there is more than one service implementing the interface (an attribute allows autowriting of services to be disabled).

The upshot of this is that you can specify less and less in the module deployment descriptor, and HiveMind will still do the right thing (or, it will provide detailed feedback about what went wrong).

The other big change in HiveMind of late is the concept of object providers. This is (as with much in HiveMind) somewhat abstract but very exciting. Previosly in HiveMind, the schema format identified the type and interpreation of attributes using translators. Is an attribute a configuration id? Then use the configuration translator. Is the attribute a service id? Use the service translator.

This is good as far as it goes, but it tends to promote a kind of propogation of elements. Look at BuilderFactory, which has a bunch of elements for different types: set, set-long, set-service, set-configuration and so forth.

This is also limiting; a property of an object (or service) to be set may be an interface, and it doesn't matter to the object whether that actual instance plugged in is a service or an arbitrary object. Given factory services (such as BeanFactory), its evident that HiveMind services aren't the only interesting objects out there. That's a problem that forces some people to cut-and-paste BuilderFactory to add new ways of supply objects to the constructed service. That's not good!

This is where the new object translator comes it. The translator is driven by a configuration point, ObjectProviders. The configuration defines different prefixes and provides services that know what to do with strings with that prefix.

So, for example, service:MyService will be intepreted as a service id, and bean:MyFactory:name will access a BeanFactory service and ask it for the name bean. There's even a service-property:MyService:propertyName option, to access a service and read a property from it.

For building services, BuilderFactory now has a set-object element. Further, since this is driven by a configuration point, it will be possible to add application-specific prefixes as well! So, once again, HiveMind is object soup, and its getting less important where those objects are coming from.

Saturday, July 10, 2004

Mozilla Gains on Internet Explorer

Slashdot is reporting that Mozilla Gains on Internet Explorer. I can say personally that I have switched entirely from IE to Firefox (0.9.1). Unlike the earlier version I tried (then named Phoenix), Firefox has been very fast, very stable and very ... fun. Just love the tabs.

I keep IE around for occasional compatibility testing, and for the couple of sites I use that don't like Mozilla, such as Shutterfly.

The only thing I miss is InstantSource, but I have contacted the authors about porting it over, and perhaps they will. I'm sure I'm not the only one. The WebDeveloper extension for Mozilla is really good, and has some great features that InstantSource doesn't have, but is not as useful as InstantSource.

Wednesday, July 07, 2004

Updated Tapestry In Action Examples

I just spent an hour or two updating the Tapestry In Action examples. This new source code distribution will be available from Manning's web site, in the meantime you can get it here (3.3 MB).

Changes:

  • Now works with and requires, Tomcat 5 (tested with 5.0.25).
  • examples.war renamed to tiaexamples.war, to not conflict with Tomcat's built-in examples.
  • No longer changes Tomcat's server.xml
  • Now includes the Tapestry 3.0 libraries and dependencies ... no seperate download needed.

Why include the libraries? Because it turns out that the exect version of the Tapestry framework is critical to getting the WAR to deploy. Initially I couldn't get tiaexamples.war to deploy and it took me forever to track it down; there's a reference to the Tapestry JAR in its web.xml:

  <taglib>
  	<taglib-uri>http://jakarta.apache.org/tapestry/tld/tapestry_1_0.tld</taglib-uri>
  	<taglib-location>/WEB-INF/lib/tapestry-3.0.jar</taglib-location>
  </taglib>
If that JAR doesn't exist (say because you tried to deploy using 3.0-rc-1) then the WAR just doesn't deploy. You have to dig through the Tomcat logs for a while before you find the problem. The best solution was to ensure that the version matches up by including all the libraries and dependencies (getting the dependencies being every newbies #1 issue). Because this distribution isn't served off of apache.org or a mirror, including the dependencies is completely legitimate.

Sunday, July 04, 2004

HiveMind and automatic reloading

I gave this another thought yesterday while relaxing on the beach. HiveMind doesn't have a way to redeploy because a descriptor changed. That wasn't on e of the problems I wanted to solver with HiveMind initially.

It's a complicated problem, because the very nature of HiveMind is to intertwine services. Service A may use configuration B that includes a bean obtained from service C that is itself driven by configuration D. If a contribution to service D changes, how does that affect service A? I suppose, in theory, you could do some tricky forward chaining so that a change to configuration D will be identified a propagated forward, invalidating service C and configuration B and ultimately service A. You could then have the proxy for service A somehow obtain a new instance that reflects the change to configuration D.

What are you willing to pay for that? The CPU and memory expense of determining what has changed and what is affected looks prohibitive to me. Something would have to poll for changes to the modules, re-parse the module descriptors, rebuild the various module descriptors, and send notifications to proxies and other objects.

The thing is, HiveMind is supposed to be light and fast. I have always pictured HiveMind running inside an EAR. When the EAR is redployed, the old HiveMind Registry is shutdown and a new HiveMind Registry is created and started to replace it. During development, just bounce your application (i.e., restart Jetty). I'm doing this today on my current project, and it's not a great imposition ... just a couple of seconds to stop and restart. The Jetty Launcher plugin makes it a single click.

So I'm still filing this under YAGNI(You Aint Gonna Need It).

Wednesday, June 23, 2004

Clever Jetty Hack: Dynamically merging directories inside a web application context

I'm working on converting an existing web site from homebrew servlets and JSPs to Tapestry. What's interesting is that the application in question is deployed into different hosts using different "skins". Certain stylesheets and images are different, and there are minor differences in layout and behavior.

So, I'm working with their workspace directory layout; there's a common directory that contains the main content (images, stylesheets, Tapestry artifacts). Then there's additional directories, such as defaultskin, that contain skin-specific assets and artifacts.

When building and deploying, the content is merged together to form a composite web application context.

For development purposes, I want to leave the directory structure intact, but that makes it impossible to run the application (the assets and artifacts in the defaultskin directory simply aren't visible. What I need is to be able to have a virtual directory within my web application that points to an entirely different directory; I want to map /skin in my context to the defaultskin folder, so I can use URLs like http://.../skin/images/about.gif.

Did a bit of research and realized that the correct approach was to create my own implementation of Jetty's WebApplicationContext that included the necessary hooks:

package portal.jetty;

import java.io.IOException;

import org.mortbay.jetty.servlet.ServletHttpContext;
import org.mortbay.jetty.servlet.WebApplicationContext;
import org.mortbay.util.Resource;

/**
 * Used only during testing using Jetty, this allows resources "within"
 * a web application context to be retrieved from an entirely different
 * directory.
 *
 * @author Howard Lewis Ship
 */
public class SkinContext extends WebApplicationContext
{
    private String _skinURI;
    private String _skinPath;
    private Resource _skinResource;

    /**
     * Standard constructor; passed a path or URL for a war (or exploded war
     * directory).
     */
    public SkinContext(String war)
    {
        super(war);
    }

    public String getSkinURI()
    {
        return _skinURI;
    }

    public void setSkinURI(String string)
    {
        _skinURI = string;
    }

    public Resource getResource(String contextPath) throws IOException
    {
        if (contextPath.startsWith(_skinURI))
        {
            String postPrefixPath = contextPath.substring(_skinURI.length());

            return _skinResource.addPath(postPrefixPath);
        }

        return super.getResource(contextPath);
    }

    public String getSkinPath()
    {
        return _skinPath;
    }

    public void setSkinPath(String string)
    {
        _skinPath = string;
    }

    public void start() throws Exception
    {
        super.start();

        _skinResource = Resource.newResource(_skinPath);
    }

}

This gets combined with a specialized jetty.xml (startup configuration file):

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure 1.2//EN" "http://jetty.mortbay.org/configure_1_2.dtd">

<!-- At deployment time, the WAR will be built properly, so that content under 
  "skin" is just appropriate to the type of site. During development, we don't want to have
  to copy files around unecessarily, so we alias the web/defaultskin directory as "/skin". -->
   
<Configure class="org.mortbay.jetty.Server">
  <Call name="addListener">
    <Arg>
      <New class="org.mortbay.http.SocketListener">
        <Set name="port">8080</Set>
      </New>
    </Arg>
  </Call>
  
  <Call name="addContext">
    <Arg/>
    <Arg>
      <New class="portal.jetty.SkinContext">
        
          <Arg>web/common</Arg>
        
        <Set name="contextPath">/</Set>
        <Set name="skinURI">/skin/</Set>
        <Set name="skinPath">web/defaultskin/</Set>
      </New>
    </Arg>
  </Call>
  
</Configure>

Viva open source! I didn't even bother pulling down the Jetty source to figure this out, I just navigated around Jetty's browse CVS to find the few code snippets. Of course, I don't think this solution is ideal for deployment, but that's not the point ... it's about making my development time efficient and I'm quite happy with it.