<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" media="screen" href="/~d/styles/atom10full.xsl"?><?xml-stylesheet type="text/css" media="screen" href="http://feeds.feedburner.com/~d/styles/itemcontent.css"?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:openSearch="http://a9.com/-/spec/opensearch/1.1/" xmlns:georss="http://www.georss.org/georss" xmlns:gd="http://schemas.google.com/g/2005" xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0" gd:etag="W/&quot;A0MMQn4_fip7ImA9WxBWFkU.&quot;"><id>tag:blogger.com,1999:blog-4110180</id><updated>2010-02-08T20:24:43.046-08:00</updated><title>Tapestry Central</title><subtitle type="html">The inside scoop on what's happening with Tapestry ... from the creator of the Apache Tapestry framework.  Plus all the normal, random thoughts on coding and technology.</subtitle><link rel="http://schemas.google.com/g/2005#feed" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/posts/default" /><link rel="alternate" type="text/html" href="http://tapestryjava.blogspot.com/" /><link rel="hub" href="http://pubsubhubbub.appspot.com/" /><link rel="next" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default?start-index=26&amp;max-results=25&amp;redirect=false&amp;v=2" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email></author><generator version="7.00" uri="http://www.blogger.com">Blogger</generator><openSearch:totalResults>672</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="self" type="application/atom+xml" href="http://feeds.feedburner.com/TapestryCentral" /><feedburner:info uri="tapestrycentral" /><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="hub" href="http://pubsubhubbub.appspot.com" /><entry gd:etag="W/&quot;CkQFQno9fCp7ImA9WxBWFko.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-1095895093618911993</id><published>2010-02-08T15:05:00.000-08:00</published><updated>2010-02-08T15:05:13.464-08:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2010-02-08T15:05:13.464-08:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="clojure" /><title>Paris Clojure Talk</title><content type="html">&lt;p&gt;I had a terrific time spreading the word about Clojure tonight, followed by some fun and spirited discussions over dinner. People are intrigued by Clojure, even as they struggled with a strategy for bringing it into their organization. 

&lt;div style="width:425px;text-align:left" id="__ss_3108191"&gt;&lt;a style="font:14px Helvetica,Arial,Sans-serif;display:block;margin:12px 0 3px 0;text-decoration:underline;" href="http://www.slideshare.net/hlship/clojure-towards-the-essence-of-programming" title="Clojure: Towards The Essence of Programming"&gt;Clojure: Towards The Essence of Programming&lt;/a&gt;&lt;object style="margin:0px" width="425" height="355"&gt;&lt;param name="movie" value="http://static.slidesharecdn.com/swf/ssplayer2.swf?doc=skillsmatter-clojure-key-100208165543-phpapp02&amp;stripped_title=clojure-towards-the-essence-of-programming" /&gt;&lt;param name="allowFullScreen" value="true"/&gt;&lt;param name="allowScriptAccess" value="always"/&gt;&lt;embed src="http://static.slidesharecdn.com/swf/ssplayer2.swf?doc=skillsmatter-clojure-key-100208165543-phpapp02&amp;stripped_title=clojure-towards-the-essence-of-programming" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="425" height="355"&gt;&lt;/embed&gt;&lt;/object&gt;&lt;div style="font-size:11px;font-family:tahoma,arial;height:26px;padding-top:2px;"&gt;View more &lt;a style="text-decoration:underline;" href="http://www.slideshare.net/"&gt;presentations&lt;/a&gt; from &lt;a style="text-decoration:underline;" href="http://www.slideshare.net/hlship"&gt;Howard Lewis Ship&lt;/a&gt;.&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;
If you attended, please &lt;a href="http://speakerrate.com/talks/2108-clojure-towards-the-essence-of-programming"&gt;rate my talk at SpeakerRate&lt;/a&gt;!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-1095895093618911993?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/wXFveViog9w" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/1095895093618911993/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=1095895093618911993" title="2 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/1095895093618911993?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/1095895093618911993?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/wXFveViog9w/paris-clojure-talk.html" title="Paris Clojure Talk" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">2</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2010/02/paris-clojure-talk.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DEUCRno4eSp7ImA9WxBWFkk.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-4307946348678520937</id><published>2010-02-08T08:22:00.000-08:00</published><updated>2010-02-08T08:24:27.431-08:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2010-02-08T08:24:27.431-08:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="tapestry" /><title>Commited to Tapestry</title><content type="html">&lt;p&gt;Quite a few people have commented on &lt;a href="http://tapestryjava.blogspot.com/2010/02/ten-years-of-tapestry.html"&gt;Ten Years of Tapestry&lt;/a&gt;, many to note some of the many other great projects being built with Tapestry as a foundation.

&lt;p&gt;
We keep a list of tutorials and extensions on &lt;a href="http://tapestry.apache.org/"&gt;the Tapestry home page&lt;/a&gt;, with many other sites noted on the wiki (&lt;a href="http://wiki.apache.org/tapestry/PoweredByTapestry"&gt;here&lt;/a&gt; and &lt;a href="http://wiki.apache.org/tapestry/SuccessStories"&gt;here&lt;/a&gt;).

&lt;p&gt;
Meanwhile, a particular comment from &lt;a href="http://www.blogger.com/profile/14104175367244532528"&gt;Peter Rietzler&lt;/a&gt; was so compelling, it deserves to be top level, so here it goes:

&lt;blockquote&gt;
&lt;p&gt;
Although our web application uses Tapestry we are using all the Tapestry support stuff far beyond the web tier - I thought that it might be interesting to see that the Tapestry framework is pretty useful in other environments too :) 

&lt;p&gt;First and most noteworthy Tapestry IOC got our first choice as dependency injection container, both because of its simplicity and the power of contributions and service overrides. We are building a highly modular (and massively unit- and integration- tested) application where Tapestry IOC's concept of modules and contributions has proven a perfect choice for us. I've written a couple of blog entries about this issue about a year ago when we were searching for a light-weight alternative to OSGi:
&lt;a href="http://peterrietzler.blogspot.com/2008/12/is-osgi-going-to-become-next-ejb-bubble.html"&gt;Is OSGi going to become the next EJB ?&lt;/a&gt;
and
&lt;a href="http://peterrietzler.blogspot.com/2009/02/how-to-design-software-for-flexibility.html"&gt;How to Design Software for Flexibility, Reusability and Scalability without loosing KISS principles!&lt;/a&gt;

&lt;p&gt;We wrote and contributed Tapestry extensions for popular unit testing frameworks: 
&lt;a href="http://www.unitils.org"&gt;Unitils&lt;/a&gt; (included in next release) and the &lt;a href="http://code.google.com/p/spock/"&gt;Spock framework&lt;/a&gt;.

&lt;p&gt;Additionally we are heavily using Tapestry services (such as pipelines and chains) in our core services. 

&lt;p&gt;Even the type coercion infrastructure has proven very useful for us. We developed a quite powerful Groovy DSL for Enterprise Data Mediation which is targeted to non-developers and we use Tapestry type coercion (with some extensions) tightly embedded in our DSL to free our e-business managers from the burden of providing correct types.

&lt;p&gt;
Our whole project heavily relies on small contributions of commands that are instantiated in high volumes at runtime and need environmental stuff injected - another point where Tapestry IOC has proven to be very useful.

&lt;p&gt;
Cheers and many thanks for your awesome work,

&lt;p&gt;
Peter

&lt;p&gt;
btw: I've forgot to mention that we presented our module system with Tapestry IOC at the &lt;a href="http://ejug.at/node/33"&gt;Austrian Enterprise Java User Group meeting&lt;/a&gt; along with another talk about Spring DM and OSGi held by Sam Brannen last autumn.
&lt;/blockquote&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-4307946348678520937?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/uxqRHZuYtG0" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/4307946348678520937/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=4307946348678520937" title="1 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/4307946348678520937?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/4307946348678520937?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/uxqRHZuYtG0/commited-to-tapestry.html" title="Commited to Tapestry" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">1</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2010/02/commited-to-tapestry.html</feedburner:origLink></entry><entry gd:etag="W/&quot;AkAMR3o9cSp7ImA9WxBWE00.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-202548239237368319</id><published>2010-02-04T10:39:00.000-08:00</published><updated>2010-02-04T10:39:46.469-08:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2010-02-04T10:39:46.469-08:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="tapestry" /><title>Ten Years of Tapestry</title><content type="html">&lt;p&gt;
I recently realized that the first prototype of Tapestry was written ten years ago! It all started as a home project in my living room, with the original inspiration coming from some brief exposure to &lt;a href="http://en.wikipedia.org/wiki/WebObjects"&gt;WebObjects&lt;/a&gt;.

&lt;p&gt;
Even the "new" codebase, Tapestry 5, is well over three years old at this point.

&lt;p&gt;
How long can I ride this dragon?  Pretty far, I think ... Tapestry keeps getting better, I keep learning new things, and the community keeps growing. I'm also very impressed by the other Tapestry committers, who have really been stepping up to the plate, not just with code, but with infrastructure issues and the backporting of bug fixes.

&lt;p&gt;
I think there are a lot of exciting things afoot in the larger Tapestry world right now. Powerful new features are in the 5.2 code base (still in alpha), including enhancements for JSR-303 (bean validation) and a lot of (backwards compatible) changes to the way component classes are enhanced at runtime. I'm also steaming ahead with a number of big improvements to how JavaScript is organized in the rendered page.

&lt;p&gt;
Outside of the core project, there's quite a lot going on. Here's a few things that have caught my attention recently:

&lt;p&gt;
&lt;img class="inline" src="http://wookicentral.com/static/img/wooki.png"/&gt;
First off, there's &lt;a href="http://wookicentral.com/"&gt;Wooki&lt;/a&gt;, a sizable Tapestry application (open source, on GitHub) for collaborative book writing. It's very pretty to look at, and the code looks quite ship-shape (no pun intended).  I think Wookie is not only going to prove useful on its own terms, but is also going to serve as a great example code base for Tapestry.

&lt;p&gt;
&lt;img class="inline" src="http://tynamo.org/download/attachments/133136534/tynamo-logo-transparentbg.png?version=1&amp;modificationDate=1259625433895"/&gt;
Next up is &lt;a href="http://tynamo.org/"&gt;Tynamo&lt;/a&gt; ... think Rails/Grails meets Tapestry. It's an extension to Tapestry that supports even faster RAD development, automatically creating CRUD (Create Read Update Delete) pages for Hibernate entities. These same people have been building REST support for Tapestry as well as conversational state. Lots of good stuff here (though I haven't had a chance to try it out in detail).

&lt;p&gt;
I've been busy with my own &lt;a href="http://github.com/hlship/tapx"&gt;Tapestry Extensions&lt;/a&gt; project at GitHub. I'm in a lucky space ... I'm adding features to Tapestry and TapX to fit my client's needs.

&lt;p&gt;
We're also seeing the deployments of some very large Tapestry 5 applications, such as &lt;a href="http://www.seesaw.com/"&gt;SeeSaw&lt;/a&gt; which is the UK's answer to Hulu ... streaming video on demand. This is expected to be one of the highest bandwidth sites in Europe once it leaves beta.

&lt;p&gt;
The shame of it is ... I'm just the creator of the framework; I don't know 1% of what's going on with applications developed in Tapestry. If you are working on something cool, please drop me a line!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-202548239237368319?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/aHB-gD1_jWU" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/202548239237368319/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=202548239237368319" title="8 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/202548239237368319?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/202548239237368319?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/aHB-gD1_jWU/ten-years-of-tapestry.html" title="Ten Years of Tapestry" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">8</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2010/02/ten-years-of-tapestry.html</feedburner:origLink></entry><entry gd:etag="W/&quot;CUUCSHk_eip7ImA9WxBWEk8.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-1548222074742575731</id><published>2010-02-03T10:54:00.000-08:00</published><updated>2010-02-03T10:54:29.742-08:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2010-02-03T10:54:29.742-08:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="skillsmatter" /><category scheme="http://www.blogger.com/atom/ns#" term="clojure" /><title>"Clojure: Towards the Essence of Programming" in Paris Feb 8th</title><content type="html">&lt;p&gt;&lt;img src="http://clojure.org/file/view/clojure-icon.gif" alt="Clojure Icon" style="float:right;"/&gt;
I'll be presenting &lt;a href="http://skillsmatter.com/event/java-jee/clojure-towards-the-essence-of-programming"&gt;Clojure: Towards the Essence of Programming&lt;/a&gt; on Monday Feb 8th at 19:00, in Paris. The event will be held at &lt;a href="http://www.zenika.com/"&gt;Zenika&lt;/a&gt;, SkillsMatter's partner in France. You &lt;em&gt;must&lt;/em&gt; &lt;a href="http://skillsmatter.com/event/java-jee/clojure-towards-the-essence-of-programming"&gt;register&lt;/a&gt; for the talk ahead of time.

&lt;p&gt;
I'm really excited about this talk; I've had a chance to "preview" it at CodeMash, and have used the feedback to make it even stronger.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-1548222074742575731?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/YevMFKYioIY" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/1548222074742575731/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=1548222074742575731" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/1548222074742575731?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/1548222074742575731?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/YevMFKYioIY/clojure-towards-essence-of-programming.html" title="&quot;Clojure: Towards the Essence of Programming&quot; in Paris Feb 8th" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2010/02/clojure-towards-essence-of-programming.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DEMCSXY9cCp7ImA9WxBXEUo.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-6957153490187958873</id><published>2010-01-22T08:07:00.000-08:00</published><updated>2010-01-22T08:07:48.868-08:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2010-01-22T08:07:48.868-08:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="tapestry" /><category scheme="http://www.blogger.com/atom/ns#" term="skillsmatter" /><title>In the Brain of Ben Gidley: Mar 23 in London</title><content type="html">&lt;p&gt;
&lt;img alt="SkillsMatter Logo" style="float: right;" src="http://skillsmatter.com/custom/images/sm-logo-black-manga.gif"&gt;
Along with my upcoming &lt;a href="http://skillsmatter.com/course-details/java-jee/tapestry-web-development"&gt;Tapestry training at SkillsMatter&lt;/a&gt;, there
is now a session w/ Ben Gidley on March 23: &lt;a href="http://skillsmatter.com/event/java-jee/tapestry-5-in-action/zx-486"&gt;In the Brain of Ben Gidley: Tapestry 5 In Action&lt;/a&gt;.  Ben will be talking about project SeeSaw, a video-on-demand service built on top of Tapestry 5. He'll be going into depth about converting Struts developers to Tapestry 5, dealing with large volumes of users and large numbers of complex pages, and many of the other factors of bringing a premier web application to deployment.
&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-6957153490187958873?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/8E2oHJTm-XI" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/6957153490187958873/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=6957153490187958873" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/6957153490187958873?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/6957153490187958873?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/8E2oHJTm-XI/in-brain-of-ben-gidley-mar-23-in-london.html" title="In the Brain of Ben Gidley: Mar 23 in London" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2010/01/in-brain-of-ben-gidley-mar-23-in-london.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DkAAQng4fSp7ImA9WxBQF0k.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-3488743337706462272</id><published>2010-01-15T12:23:00.001-08:00</published><updated>2010-01-17T08:12:23.635-08:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2010-01-17T08:12:23.635-08:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="clojure" /><category scheme="http://www.blogger.com/atom/ns#" term="codemash" /><title>CodeMash: Clojure</title><content type="html">&lt;p&gt;Another great talk about Clojure and functional programming; larger room, more people, great (and difficult) questions.  I made James Ward's head hurt.

&lt;div style="width:425px;text-align:left" id="__ss_2934568"&gt;&lt;a style="font:14px Helvetica,Arial,Sans-serif;display:block;margin:12px 0 3px 0;text-decoration:underline;" href="http://www.slideshare.net/hlship/codemashclojurepdf" title="Codemash-Clojure.pdf"&gt;Codemash-Clojure.pdf&lt;/a&gt;&lt;object style="margin:0px" width="425" height="355"&gt;&lt;param name="movie" value="http://static.slidesharecdn.com/swf/ssplayer2.swf?doc=codemashclojurepdf3259&amp;rel=0&amp;stripped_title=codemashclojurepdf" /&gt;&lt;param name="allowFullScreen" value="true"/&gt;&lt;param name="allowScriptAccess" value="always"/&gt;&lt;embed src="http://static.slidesharecdn.com/swf/ssplayer2.swf?doc=codemashclojurepdf3259&amp;rel=0&amp;stripped_title=codemashclojurepdf" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="425" height="355"&gt;&lt;/embed&gt;&lt;/object&gt;&lt;div style="font-size:11px;font-family:tahoma,arial;height:26px;padding-top:2px;"&gt;View more &lt;a style="text-decoration:underline;" href="http://www.slideshare.net/"&gt;documents&lt;/a&gt; from &lt;a style="text-decoration:underline;" href="http://www.slideshare.net/hlship"&gt;Howard Lewis Ship&lt;/a&gt;.&lt;/div&gt;&lt;/div&gt;


&lt;p&gt;If you attended, please &lt;a href="http://speakerrate.com/talks/1943"&gt;rate my talk at SpeakerRate&lt;/a&gt;.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-3488743337706462272?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/LHlvxR6j9-0" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/3488743337706462272/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=3488743337706462272" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/3488743337706462272?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/3488743337706462272?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/LHlvxR6j9-0/codemash-clojure-talk.html" title="CodeMash: Clojure" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2010/01/codemash-clojure-talk.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DkEAQ3k4eyp7ImA9WxBQF0k.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-8386586196452766291</id><published>2010-01-15T07:50:00.000-08:00</published><updated>2010-01-17T08:10:42.733-08:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2010-01-17T08:10:42.733-08:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="tapestry" /><category scheme="http://www.blogger.com/atom/ns#" term="codemash" /><title>CodeMash: Tapestry</title><content type="html">&lt;p&gt;
Just finished my talk on Tapestry at CodeMash ... very fun, very successful, very good questions and we finished (just barely) on time.

&lt;div style="width:425px;text-align:left" id="__ss_2934561"&gt;&lt;a style="font:14px Helvetica,Arial,Sans-serif;display:block;margin:12px 0 3px 0;text-decoration:underline;" href="http://www.slideshare.net/hlship/codemashtapestrypdf" title="Codemash-Tapestry.pdf"&gt;Codemash-Tapestry.pdf&lt;/a&gt;&lt;object style="margin:0px" width="425" height="355"&gt;&lt;param name="movie" value="http://static.slidesharecdn.com/swf/ssplayer2.swf?doc=codemashtapestrypdf2248&amp;rel=0&amp;stripped_title=codemashtapestrypdf" /&gt;&lt;param name="allowFullScreen" value="true"/&gt;&lt;param name="allowScriptAccess" value="always"/&gt;&lt;embed src="http://static.slidesharecdn.com/swf/ssplayer2.swf?doc=codemashtapestrypdf2248&amp;rel=0&amp;stripped_title=codemashtapestrypdf" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="425" height="355"&gt;&lt;/embed&gt;&lt;/object&gt;&lt;div style="font-size:11px;font-family:tahoma,arial;height:26px;padding-top:2px;"&gt;View more &lt;a style="text-decoration:underline;" href="http://www.slideshare.net/"&gt;documents&lt;/a&gt; from &lt;a style="text-decoration:underline;" href="http://www.slideshare.net/hlship"&gt;Howard Lewis Ship&lt;/a&gt;.&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;
Please jump over to &lt;a href="http://speakerrate.com/talks/1942"&gt;SpeakerRate&lt;/a&gt; to rate or comment on the talk.  Feedback is very welcome!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-8386586196452766291?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/JYGuoUI25pQ" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/8386586196452766291/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=8386586196452766291" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/8386586196452766291?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/8386586196452766291?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/JYGuoUI25pQ/codemash-tapestry.html" title="CodeMash: Tapestry" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2010/01/codemash-tapestry.html</feedburner:origLink></entry><entry gd:etag="W/&quot;Ak4FQXg9cSp7ImA9WxBQFU0.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-2962866514209360483</id><published>2010-01-14T14:41:00.000-08:00</published><updated>2010-01-14T14:41:50.669-08:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2010-01-14T14:41:50.669-08:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="tapestry" /><category scheme="http://www.blogger.com/atom/ns#" term="javassist" /><title>Tapestry and Bytecode Generation</title><content type="html">&lt;p&gt;
On the flight out to CodeMash I started working on some refinements to how Tapestry does runtime class transformation. My long term goal is to move away from Javassist and towards something a bit simpler ... like ASM.  Why?  Javassist does not have a good, responsive, supportive community, and it has been increasingly flaky since JDK 1.6.

&lt;p&gt;
Previously, I've blogged about &lt;a href="http://tapestryjava.blogspot.com/2006/09/javassist-vs-every-other-bytecode.html"&gt;how invaluable Javassist is&lt;/a&gt;, and I stand behind that early pronouncement. Tapestry IoC and Tapestry Core both use Javassist extensively to create new classes at runtime, as well as modify classes as they are loaded into memory.  However, I've also been &lt;a href="http://tapestryjava.blogspot.com/2009/08/article-meta-programming-java.html"&gt;finding new ways to apply meta-programming without exposing all the gory details of Javassist&lt;/a&gt;. As usual, &lt;em&gt;simplicity follows complexity&lt;/em&gt;: I'm finding ways to simplify work I've done the hard way, previously, to make these techniques easier for others to leverage.

&lt;p&gt;
That's the pattern I'm trying for: none of the explicit Java psuedo-code used by Javassist, instead, defining ways to add behavior to methods, or individual fields, in terms of simple callback interfaces. For the moment, the under-the-covers wiring is still Javassist (underneath the Tapestry ComponentClassTransformation interface), but eventually all the parts that are truly tied to Javassist (i.e., those parts of the API where a Javassist pseudo-code string are provided) can be phased out, deprecated, and eliminated.

&lt;p&gt;
The advantage of this revised approach is that the amount of runtime-generated code decreases and simplifies. Less behavior is created via Javassist pseudo-code, and fewer fields need to be created or injected into the component class. Further, more runtime code will be in standard objects, compiled by the standard Java compiler, and less code will be compiled by Javassist. Intuitively speaking (always dangerous), it makes sense that standard Java code will be optimized better by Hotspot: Reportedly, some aspects of Hotspot are tied to the exact form of bytecode produced by the Sun Java compiler).

&lt;p&gt;
I've heard from some specific Tapestry users who are building and deploying &lt;em&gt;very&lt;/em&gt; large, very complicated applications, that live class reloading is problematic for them to use: their pages consist of hundreds (possibly thousands) of deeply nested components, and they are seeing 30+ second delays reloading a page after a change.  Whenever a component class changes, Tapestry must discard the old ClassLoader, and create a new one, and lazily re-instrument all the component classes; this isn't a big deal with only dozens of pages and components, but I want Tapestry to be effective even for the largest, most complicated web applications. Simplifying and revising Tapestry's approach to bytecode enhancement is just the latest in a series of internal changes targeting improved performance.

&lt;p&gt;
Meanwhile. the CodeMash conference goes on around me ... and shortly, back to the waterpark.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-2962866514209360483?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/r_BCxeNHHKU" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/2962866514209360483/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=2962866514209360483" title="2 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/2962866514209360483?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/2962866514209360483?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/r_BCxeNHHKU/tapestry-and-bytecode-generation.html" title="Tapestry and Bytecode Generation" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">2</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2010/01/tapestry-and-bytecode-generation.html</feedburner:origLink></entry><entry gd:etag="W/&quot;CkMFR38yeyp7ImA9WxBQFEQ.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-1625398817463307812</id><published>2010-01-14T09:33:00.000-08:00</published><updated>2010-01-14T09:33:36.193-08:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2010-01-14T09:33:36.193-08:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="codemash" /><title>CodeMash Schedule Change</title><content type="html">&lt;p&gt;
There's a late change for my talk at &lt;a href="http://codemash.org/"&gt;CodeMash 2.0.1.0&lt;/a&gt;&lt;img src="http://www.codemash.org/images/layout/logo-codemash.gif" alt="CodeMash" style="float:right;"/&gt;: my Clojure talk is now Friday at 1:45 in room D.  Aaron's talk on Compojure follows at 3:35 in the same room ... which is good, because I can explain a bit about Clojure and it's syntax and Aaron can follow on with a practical application.  It's a good change.

&lt;p&gt;
I'll be posting my slides up to Slidehsare after the sessions; check back here for links to that, and to the SpeakerRate site.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-1625398817463307812?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/cZUqWuKP3vU" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/1625398817463307812/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=1625398817463307812" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/1625398817463307812?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/1625398817463307812?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/cZUqWuKP3vU/codemash-schedule-change.html" title="CodeMash Schedule Change" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2010/01/codemash-schedule-change.html</feedburner:origLink></entry><entry gd:etag="W/&quot;CEMGRns5cCp7ImA9WxBRE00.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-5917625507823237004</id><published>2009-12-31T15:33:00.000-08:00</published><updated>2009-12-31T15:33:47.528-08:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-12-31T15:33:47.528-08:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="clojure" /><title>Clojure 1.1 is out ... plus videos about new features</title><content type="html">&lt;p&gt;
So, Clojure 1.1 is now available, with lots of cool new features, including transients, pre &amp; post conditions, futures, promises and a boat load of other stuff. Rich Hickey has put together &lt;a href="http://github.com/richhickey/clojure/blob/1.1.x/changes.txt"&gt;release notes&lt;/a&gt;.

&lt;p&gt;
Meanwhile, if you are curious about some of these new features, check out
this  &lt;a href="http://vimeo.com/8320428"&gt;series of videos by Sean Devlin&lt;/a&gt;.

&lt;p&gt;
&lt;a href="http://www.codemash.org/"&gt;
&lt;img src="http://www.codemash.org/images/layout/logo-codemash.gif" alt="CodeMash" style="float:right;"/&gt;&lt;/a&gt;
I'll be speaking about &lt;a href="http://www.codemash.org/Sessions#Clojure%3a+Concurrent+Functional+Programming+for+the+JVM"&gt;Clojure&lt;/a&gt; and &lt;a href="http://www.codemash.org/Sessions#Tapestry+5%3a+Java+Power%2c+Scripting+Ease"&gt;Tapestry&lt;/a&gt; at 
&lt;a href="http://www.codemash.org/"&gt;CodeMash 2.0.1.0&lt;/a&gt; this year, January 13-15.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-5917625507823237004?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/8r1heP0so1U" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/5917625507823237004/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=5917625507823237004" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/5917625507823237004?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/5917625507823237004?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/8r1heP0so1U/clojure-11-is-out-plus-videos-about-new.html" title="Clojure 1.1 is out ... plus videos about new features" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2009/12/clojure-11-is-out-plus-videos-about-new.html</feedburner:origLink></entry><entry gd:etag="W/&quot;C0MBRXYzeyp7ImA9WxBREEk.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-3876173360618241586</id><published>2009-12-28T14:49:00.000-08:00</published><updated>2009-12-28T15:04:14.883-08:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-12-28T15:04:14.883-08:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="tapestry" /><category scheme="http://www.blogger.com/atom/ns#" term="security" /><title>Securing Tapestry pages with Annotations, Part 1</title><content type="html">&lt;p&gt;Everyone wants all sorts of integrations for Tapestry with other frameworks, but sometimes rolling your own is actually easier. Let's start with securing access to pages, a subject that &lt;em&gt;still&lt;/em&gt; keeps coming up on the mailing list. I thought I'd show a little bit about how I tackle this problem generally. 

&lt;p&gt;People have been asking for a single definitive solution for handling security ... but I don't see any single solution satisfying even the majority of projects.
Why?  Because there are simply too many variables. For example, are you using LDAP, OpenAuth or some ad-hoc user registry (in your database)?  Are pages accessible by default, or in-accessible by default?  Are you using role-based security? How do you represent roles then? Creating a single solution that's pluggable enough for all these possibilities seems like an insurmountable challenge ... but perhaps we can come up with a &lt;em&gt;toolkit&lt;/em&gt; so that you can assemble your own custom solution (more on that later).

&lt;p&gt;
One approach to security could be to define a base class, &lt;code&gt;ProtectedPage&lt;/code&gt;, that enforced the basic rules (you must be logged in to use this page). You can accomplish such a thing using the &lt;code&gt;activate&lt;/code&gt; event handler ... but I find such an approach clumsy. Anytime you can avoid inheritance, you'll find your code easier to understand, easier to manage, easier to test and easier to evolve.

&lt;p&gt;
Instead, let's pursue a more declarative approach, where we use an annotation to mark pages that require that the user be logged in.  We'll start with these ground rules:
&lt;ul&gt;
&lt;li&gt;Pages are freely accessible by anyone, unless they have a @RequiresLogin annotation
&lt;li&gt;Any static resource (in the web context directory) is accessible to anybody
&lt;li&gt;There's already some kind of UserAuthentication service that knows if the user is currently logged in or not, and (if logged in) who they are, as a User object
&lt;/ul&gt;

&lt;p&gt;
So, we need to define a RequiresLogin annotation, and we need to enforce it, by preventing any access to the page unless the user is logged in.

&lt;p&gt;
That poses a challenge: how do you get "inside" Tapestry to enforce this annotation? What you really want to do is "slip in" a little bit of your code into existing Tapestry code ... the code that analyzes the incoming request, determines what type of request it is (a page render request vs. a component event request), and ultimately starts calling into the page code to do the work.

&lt;p&gt;
This is a great example of the central design of Tapestry and it's IoC container: to natively supporting this kind of extensibility. Through the use of &lt;a href="http://tapestryjava.blogspot.com/2008/09/tapestry-5-ioc-introducing-service.html"&gt;service configurations&lt;/a&gt; it's possible to do exactly that: slip a piece of code into the middle of that default Tapestry code. The trick is to identify where.  This image gives a rough map to how Tapestry handles incoming requests:

&lt;p&gt;
&lt;img src="http://tapestry.formos.com/nightly/tapestry5/images/tapestry_request_processing_800.png" alt="Tapestry Request Processing"/&gt;

&lt;p&gt;
In fact, there's a specific place for this kind of extension: the &lt;code&gt;ComponentRequestHandler&lt;/code&gt; pipeline service&lt;a href="#secur1_1"&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;.  As a pipeline service, &lt;code&gt;ComponentRequestHandler&lt;/code&gt; has a configuration of filters, and adding a filter to this pipeline is just what we need.

&lt;h2&gt;Defining the Annotation&lt;/h2&gt;

&lt;p&gt;
First, lets define our annotation:

&lt;p&gt;
&lt;pre&gt;
@Target( { ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequiresLogin {

}
&lt;/pre&gt;

&lt;p&gt;
This annotation is designed to be placed on a page class to indicate that the user must be logged in to access the page.  The retention policy is important here: it needs to be visible at runtime for our runtime code to see it and act on its presence.

&lt;p&gt;
An annotation by itself does nothing ... we need the code that checks for the annotation.

&lt;h2&gt;Creating a ComponentRequestFilter&lt;/h2&gt;

&lt;p&gt;
Filters for the &lt;code&gt;ComponentRequestHandler&lt;/code&gt; pipeline are instances of the interface &lt;code&gt;&lt;a href="http://tapestry.apache.org/tapestry5.1/apidocs/org/apache/tapestry5/services/ComponentRequestFilter.html"&gt;ComponentRequestFilter&lt;/a&gt;&lt;/code&gt;:

&lt;pre&gt;
/**
 * Filter interface for {@link org.apache.tapestry5.services.ComponentRequestHandler}.
 */
public interface ComponentRequestFilter
{
    /**
     * Handler for a component action request which will trigger an event on a component and use the return value to
     * send a response to the client (typically, a redirect to a page render URL).
     *
     * @param parameters defining the request
     * @param handler    next handler in the pipeline
     */
    void handleComponentEvent(ComponentEventRequestParameters parameters, ComponentRequestHandler handler)
            throws IOException;

    /**
     * Invoked to activate and render a page. In certain cases, based on values returned when activating the page, a
     * {@link org.apache.tapestry5.services.ComponentEventResultProcessor} may be used to send an alternate response
     * (typically, a redirect).
     *
     * @param parameters defines the page name and activation context
     * @param handler    next handler in the pipeline
     */
    void handlePageRender(PageRenderRequestParameters parameters, ComponentRequestHandler handler) throws IOException;
}
&lt;/pre&gt;

&lt;p&gt;
Our implementation of this filter will check the page referenced in the request to see if it has the annotation. If the annotation is present and the user has not yet logged in, we'll redirect to the Login page. When a redirect is not necessary, we delegate to the next handler in the pipeline&lt;sup&gt;&lt;a href="#secur1_2"&gt;2&lt;/a&gt;&lt;/sup&gt;:

&lt;pre&gt;
public class RequiresLoginFilter implements ComponentRequestFilter {

  private final PageRenderLinkSource renderLinkSource;

  private final ComponentSource componentSource;

  private final Response response;

  private final AuthenticationService authService;

  public PageAccessFilter(PageRenderLinkSource renderLinkSource,
      ComponentSource componentSource, Response response,
      AuthenticationService authService) {
    this.renderLinkSource = renderLinkSource;
    this.componentSource = componentSource;
    this.response = response;
    this.authService = authService;
  }

  public void handleComponentEvent(
      ComponentEventRequestParameters parameters,
      ComponentRequestHandler handler) throws IOException {

    if (dispatchedToLoginPage(parameters.getActivePageName())) {
      return;
    }

    handler.handleComponentEvent(parameters);

  }

  public void handlePageRender(PageRenderRequestParameters parameters,
      ComponentRequestHandler handler) throws IOException {

    if (dispatchedToLoginPage(parameters.getLogicalPageName())) {
      return;
    }

    handler.handlePageRender(parameters);
  }

  private boolean dispatchedToLoginPage(String pageName) throws IOException {

    if (authService.isLoggedIn()) {
      return false;
    }

    Component page = componentSource.getPage(pageName);

    if (! page.getClass().isAnnotationPresent(RequiresLogin.class)) {
      return false;
    }

    Link link = renderLinkSource.createPageRenderLink("Login");

    response.sendRedirect(link);

    return true;
  }
}
&lt;/pre&gt;

&lt;p&gt;
The above code makes a bunch of assumptions and simplifications. First, it assumes the name of the page to redirect to is "Login". It also doesn't try to capture any part of the incoming request to allow the application to continue after the user logs in. Finally, the AuthenticationService is not part of Tapestry ... it is something specific to the application.

&lt;p&gt;
You'll notice that the dependencies (&lt;code&gt;PageRenderLinkSource&lt;/code&gt;, etc.)
are &lt;em&gt;injected&lt;/em&gt; through constructor parameters and then stored in final fields. This is the preferred, if more verbose approach. We could also have used no constructor, a non-final fields with an @Inject annotation (it's largely a style choice, though constructor injection with final fields is more guaranteed to be fully thread safe).

&lt;p&gt;
The class on its own is not enough, however: we have to get Tapestry to actually &lt;em&gt;use&lt;/em&gt; this class. 

&lt;h2&gt;Contributing the Filter&lt;/h2&gt;

&lt;p&gt;
The last part of this is hooking the above code into the flow. This is done by making a &lt;em&gt;contribution&lt;/em&gt; to the &lt;code&gt;ComponentEventHandler&lt;/code&gt; service's &lt;em&gt;configuration&lt;/em&gt;.

&lt;p&gt;
Service contributions are implemented as methods of a Tapestry module class, such as &lt;code&gt;AppModule&lt;/code&gt;:

&lt;pre&gt;
  public static void contributeComponentRequestHandler(
      OrderedConfiguration&lt;ComponentRequestFilter&gt; configuration) {
    configuration.addInstance("RequiresLogin", RequiresLoginFilter.class);
  }
&lt;/pre&gt;

&lt;p&gt;
Contributing modules contribute into an &lt;code&gt;OrderedConfiguration&lt;/code&gt;: after all modules have had a chance to contribute, the configuration is converted into a &lt;code&gt;List&lt;/code&gt; that's passed to the service implementation.

&lt;p&gt;
The &lt;code&gt;addInstance()&lt;/code&gt; method makes it easy to contribute the filter: Tapestry will look at the class, see the constructor, and inject dependencies into the filter via the constructor parameters. It's all very declarative: the code needs the &lt;code&gt;PageRenderLinkSource&lt;/code&gt;, so it simply defines a final field and a constructor parameter ... Tapestry takes care of the rest.

&lt;p&gt;
You might wonder why we need to specify a name ("RequiresLogin") for the contribution?  The answer addresses a somewhat rare but still important case: multiple contributions to the same configuration that have some form of interaction.  By giving each contribution a unique id, it's possible to set up ordering rules (such as "contribution 'Foo' comes after contribution 'Bar'").  Here, there is no need for ordering because there aren't any other filters (Tapestry provides this service and configuration, but doesn't make any contributions of its own into it).

&lt;h2&gt;Improvements and Conclusions&lt;/h2&gt;

&lt;p&gt;
This is just a first pass at security. For my clients, I've built more elaborate solutions, that include capturing the page name and activation context to allow the application to "resume" after the login is complete, as well as approaches for automatically logging the user in as needed (via a cookie or other mechanism).

&lt;p&gt;
Other improvements would be to restrict access to pages based on some set of user roles; again, how this is represented both in code and annotations, and in the data model is quite up for grabs.

&lt;p&gt;
My experience with different clients really underscores what a fuzzy world security can be: there are so many options for how you represent, identify and authenticate the user.  Even basic decisions are underpinnings are subject to interpretation; for example, one of my clients wants all pages to require login &lt;em&gt;unless&lt;/em&gt; a specific annotation is found. Perhaps over time enough of these use cases can be worked out to build the toolkit I mentioned earlier.

&lt;p&gt;
Even so, the amount of code to build a solid, custom security implementation is still quite small ... though the trick, as always, is writing &lt;em&gt;just&lt;/em&gt; the write code and hooking it into Tapestry in &lt;em&gt;just&lt;/em&gt; the right way.

&lt;p&gt;
I expect to follow up this article with part 2, which will expand on the solution a bit more, addressing some more of the real world constraints my customers demand. Stay tuned!

&lt;hr&gt;

&lt;p id="secur1_1"&gt;&lt;sup&gt;1&lt;/sup&gt; In fact, this service and pipeline were created in Tapestry 5.1 specifically to address this use case. In Tapestry 5.0, this approach required two very similar filter contributions to two similar pipelines.

&lt;p id="secur1_2"&gt;&lt;sup&gt;2&lt;/sup&gt; If there are multiple filters, you'd think that you'd delegate to the next filter. Actually you do, but Tapestry provides a &lt;em&gt;bridge&lt;/em&gt;: a wrapper around the filter that uses the main interface for the service. In this way, each filter delegates to either the next filter, or the &lt;em&gt;terminator&lt;/em&gt; (the service implementation after all filters) in a uniform manner. More details about this are in the &lt;a href="http://tapestry.apache.org/tapestry5.1/tapestry-ioc/pipeline.html"&gt;pipeline documentation&lt;/a&gt;.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-3876173360618241586?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/6bnk8GxvmfE" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/3876173360618241586/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=3876173360618241586" title="7 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/3876173360618241586?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/3876173360618241586?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/6bnk8GxvmfE/securing-tapestry-pages-with.html" title="Securing Tapestry pages with Annotations, Part 1" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">7</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2009/12/securing-tapestry-pages-with.html</feedburner:origLink></entry><entry gd:etag="W/&quot;AkQDQXc8eCp7ImA9WxBTGEw.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-2453146269114860033</id><published>2009-12-14T11:19:00.000-08:00</published><updated>2009-12-14T11:19:30.970-08:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-12-14T11:19:30.970-08:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="tapestry" /><category scheme="http://www.blogger.com/atom/ns#" term="training" /><title>Upcoming Public Training: London and Paris</title><content type="html">&lt;p&gt;
&lt;img src="http://skillsmatter.com/custom/images/sm-logo-black-manga.gif" style="float:right;" alt="SkillsMatter Logo"/&gt;
This is a big announcement ... something I've been working on pretty much since I left Formos.  I've partnered up with &lt;a href="http://skillsmatter.com/"&gt;SkillsMatter&lt;/a&gt; to provide my three-day, hands-on &lt;a href="http://skillsmatter.com/course-details/java-jee/tapestry-web-development"&gt;Tapestry training as a public enrollment course&lt;/a&gt;!

&lt;p&gt;This is the &lt;em&gt;exact same&lt;/em&gt; course I provide as on-site training, but we'll be doing it at the SkillsMatter offices in London on February 10th, and then in Paris on the 15th.

&lt;p&gt;
This is a big experiment for me and for SkillsMatter in terms of growing the size of the Tapestry community. In fact, SkillsMatter has really upped the ante here by offering 2-for-1 on the London training ... that's a great way to kick things off!

&lt;p&gt;
I can't emphasize enough what a great opportunity this is for people to get accelerated Tapestry training at a discount (even before factoring in the 2-for-1 offer). I'm really looking forward to bringing many new developers into the fold!

&lt;p&gt;
In addition, there will be a special, free evening event at each location. Details on that to follow. I look forward to meeting even more of you there!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-2453146269114860033?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/nYTSnBas7OA" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/2453146269114860033/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=2453146269114860033" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/2453146269114860033?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/2453146269114860033?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/nYTSnBas7OA/upcoming-public-training-london-and.html" title="Upcoming Public Training: London and Paris" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2009/12/upcoming-public-training-london-and.html</feedburner:origLink></entry><entry gd:etag="W/&quot;CUMNQ3szfyp7ImA9WxNaGUg.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-1564597564828112585</id><published>2009-12-04T11:04:00.000-08:00</published><updated>2009-12-04T11:04:52.587-08:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-12-04T11:04:52.587-08:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="devoxx" /><title>Devoxx Videos up at Parleys.com</title><content type="html">&lt;p&gt;
My sessions for this year's Devoxx are available from Parleys.com: &lt;a href="http://devoxx.parleys.com/#st=5&amp;sl=6&amp;id=1700"&gt;Tapestry&lt;/a&gt; and &lt;a href="http://devoxx.parleys.com/#st=5&amp;id=1518"&gt;Clojure&lt;/a&gt;.  You can watch the first two minutes for free (so if you idea of fun is to watch me check with the sound guy and look uncomfortable while waiting for the session to start ... you are in like Flynn); after that it's a subscription service to gain access to all of the Parleys.com content.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-1564597564828112585?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/fXOwgXouKw0" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/1564597564828112585/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=1564597564828112585" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/1564597564828112585?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/1564597564828112585?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/fXOwgXouKw0/devoxx-videos-up-at-parleyscom.html" title="Devoxx Videos up at Parleys.com" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2009/12/devoxx-videos-up-at-parleyscom.html</feedburner:origLink></entry><entry gd:etag="W/&quot;CkAMR3s6eSp7ImA9WxNaGUg.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-386145925038513064</id><published>2009-12-04T10:03:00.000-08:00</published><updated>2009-12-04T10:19:46.511-08:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-12-04T10:19:46.511-08:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="arduino" /><title>Change of pace: Arduino</title><content type="html">As a total change of pace, I've been playing around with actual &lt;i&gt;hardware&lt;/i&gt;, in the form of an &lt;a href="http://www.arduino.cc/"&gt;Arduino&lt;/a&gt; board. This is a hoot, an actual computer that you can carry around in your hand. They call it physical computing.
&lt;p&gt;
I'm just getting started with it, in my tiny shards of free time. Your code is in C (and a smattering of C++). I've hooked up four LEDs and two buttons that allow me to cycle the LEDs forward or backward. Since you're working at such a low level, you have to be aware of tiny factors such as key bounce (closing a switch will, for a short period, yield unstable results due to physical and electrical factors).
&lt;p&gt;
There's a tool called &lt;a href="http://fritzing.org/"&gt;Fritzing&lt;/a&gt; to help you document your projects. It's very alpha, but the simple results are rather nice:
&lt;p&gt;
&lt;div class="separator" style="clear: both; text-align: center;"&gt;
&lt;a href="http://2.bp.blogspot.com/_OZa40fWwJDM/SxlNhTS1_eI/AAAAAAAAACw/lztqnEuh7Kw/s1600-h/cyclea_schematic.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/_OZa40fWwJDM/SxlNhTS1_eI/AAAAAAAAACw/lztqnEuh7Kw/s400/cyclea_schematic.png" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/div&gt;
The code is still evolving:

&lt;p&gt;&lt;pre&gt;#define FIRST_LED 12
#define LED_COUNT 4
#define DEBOUNCE_PERIOD 50 // ms

class Debounce
{
public:
  Debounce(int pin);
  boolean read();
private:
  int _pin;
  int _previousValue;
  int _lastButtonDebounce;
  boolean _enabled;
};

Debounce::Debounce(int pin)
{
  _pin = pin;
  _previousValue = LOW;
  _lastButtonDebounce = 0; // never
  _enabled = true;

  pinMode(_pin, INPUT);  
}

boolean Debounce::read()
{
  int currentValue = digitalRead(_pin);

  long now = millis();

  if (currentValue != _previousValue)
  {
    _lastButtonDebounce = now;
    _previousValue = currentValue;
    return false;
  }

  if (now - _lastButtonDebounce &amp;lt; DEBOUNCE_PERIOD) {
    return false;
  }


  // It's gone HIGH to LOW

  if (currentValue == LOW) {
    _enabled = true;
    return false;
  }

  // It's gone LOW to HIGH

  if (_enabled) {
    _enabled = false;
    return true;
  }

  return false;
}

// First press will move to the first LED.
int currentLed = -1;

Debounce advanceButton = Debounce(2);
Debounce retreatButton = Debounce(3);

void setup()
{
  for (int i = 0; i &amp;lt; LED_COUNT; i++) {
    int pin = FIRST_LED - i;
    pinMode(pin, OUTPUT);
    digitalWrite(pin, HIGH);
  }

  delay(250);

  for (int i = 0; i &amp;lt; LED_COUNT; i++)
  {
    digitalWrite(FIRST_LED - i, LOW);
  }
}

void advance()
{
  if (currentLed &amp;gt;= 0)
    digitalWrite(FIRST_LED - currentLed, LOW);

  if (++currentLed == LED_COUNT)
    currentLed = 0;

  digitalWrite(FIRST_LED - currentLed, HIGH);
}

void retreat()
{
  if (currentLed &amp;gt;= 0)
    digitalWrite(FIRST_LED - currentLed, LOW);

  if (--currentLed &amp;lt; 0)
    currentLed = LED_COUNT - 1;

  digitalWrite(FIRST_LED - currentLed, HIGH);
}

void loop()
{
  if (advanceButton.read()) advance();

  if (retreatButton.read()) retreat();
}
&lt;/pre&gt;


&lt;p&gt;
Fun stuff ... and I don't see me writing a web framework for it, which is a change of pace. 

&lt;p&gt;
My ultimate goal is to write Arduino apps in a Clojure DSL, and have Clojure generate the machine code for the AVR processor that runs the Arduino. Of course, that means learning AVR machine code and writing a lot of code to compile and assemble the DSL into something that can execute inside the Arduino.

&lt;p&gt;Plan B: Perhaps its time to learn &lt;a href="http://krue.net/avrforth/"&gt;Forth&lt;/a&gt;?&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-386145925038513064?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/PdOiISvzTRw" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/386145925038513064/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=386145925038513064" title="2 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/386145925038513064?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/386145925038513064?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/PdOiISvzTRw/change-of-pace-arduino.html" title="Change of pace: Arduino" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://2.bp.blogspot.com/_OZa40fWwJDM/SxlNhTS1_eI/AAAAAAAAACw/lztqnEuh7Kw/s72-c/cyclea_schematic.png" height="72" width="72" /><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">2</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2009/12/change-of-pace-arduino.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DUAGRng9eip7ImA9WxBTEU8.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-8487338795694540626</id><published>2009-12-03T18:08:00.000-08:00</published><updated>2009-12-06T11:28:47.662-08:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-12-06T11:28:47.662-08:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="tapestry" /><category scheme="http://www.blogger.com/atom/ns#" term="captcha" /><title>Tapestry and Kaptcha</title><content type="html">&lt;p&gt;Another bit of interesting work I did, for another client, was to implement a &lt;a href="http://en.wikipedia.org/wiki/Captcha"&gt;CAPTCHA&lt;/a&gt; system. I chose the library
&lt;a href="http://code.google.com/p/kaptcha/"&gt;Kaptcha&lt;/a&gt; and built services and components around it.

&lt;p&gt;
If you follow the documentation for Katpcha, you'll see that you're supposed to configure it inside web.xml and add a servlet. That's not the Tapestry way, especially for something that will likely be split off into its own library at some point and there's no reason that all the necessary plumbing can't occur within the context of Tapestry's APIs.

&lt;p&gt;
The essence of a CAPTCHA is two fold: first, a secret string is generated on the server side. On the client-side, an image and a text field are displayed. The image is a distorted version of the secret text. The user must type the text ... humans being better able to pull meaning out of the distortion than any typical program.

&lt;p&gt;
Back on the server side, we compare what the user entered against the secret string.

&lt;p&gt;
I broke the implementation up into three pieces:
&lt;ul&gt;
&lt;li&gt;A Tapestry service to handle generating the secret string and the image
&lt;li&gt;A Tapestry component to display the image
&lt;li&gt;A second component to handle the text field
&lt;/ul&gt;

&lt;p&gt;
In practice, all it takes to use this is the following:

&lt;pre&gt;
  &amp;lt;t:kaptchaimage t:id="kaptcha"/&amp;gt;
  &amp;lt;br/&amp;gt;
  &amp;lt;t:kaptchafield image="kaptcha"/&amp;gt;
&lt;/pre&gt;

&lt;p&gt;
The two components work together to select the secret word, display the image, and validate that the user has entered the expected value.

&lt;p&gt;Let's look at how this all comes together. 

&lt;h2&gt;KaptchaProducer Service&lt;/h2&gt;

&lt;p&gt;
Kaptcha includes an interface, Producer, that has most of what I want:

&lt;pre&gt;
package com.google.code.kaptcha;

import java.awt.image.BufferedImage;

/**
 * Responsible for creating captcha image with a text drawn on it.
 */
public interface Producer
{
  /**
   * Create an image which will have written a distorted text.
   * 
   * @param text
   *            the distorted characters
   * @return image with the text
   */
  BufferedImage createImage(String text);

  /**
   * @return the text to be drawn
   */
  String createText();
}
&lt;/pre&gt;

&lt;p&gt;
I extended this to add methods for determining the width and height of the captcha image:

&lt;pre&gt;
package com.myclient.services.kaptcha;

import com.google.code.kaptcha.Producer;

/**
 * Extension of KatpchaProducer that exposes the images width and height (in
 * pixels).
 * 
 */
public interface KaptchaProducer extends Producer {

  int getWidth();

  int getHeight();
}
&lt;/pre&gt;

&lt;p&gt;
My implementation is largely a wrapper around Kaptcha's default implementation:

&lt;pre&gt;
package com.myclient.services.kaptcha;

import java.awt.image.BufferedImage;
import java.util.Map;
import java.util.Properties;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;

public class KaptchaProducerImpl implements KaptchaProducer {

  private final DefaultKaptcha producer;

  private final int height;

  private final int width;

  public KaptchaProducerImpl(Map&amp;lt;String, String&amp;gt; configuration) {
    producer = new DefaultKaptcha();

    Config config = new Config(toProperties(configuration));

    producer.setConfig(config);

    height = config.getHeight();
    width = config.getWidth();
  }

  public int getHeight() {
    return height;
  }

  public int getWidth() {
    return width;
  }

  public BufferedImage createImage(String text) {
    return producer.createImage(text);
  }

  public String createText() {
    return producer.createText();
  }

  private static Properties toProperties(Map&amp;lt;String, String&amp;gt; map) {

    Properties result = new Properties();

    for (String key : map.keySet()) {
      result.put(key, map.get(key));
    }

    return result;

  }
}
&lt;/pre&gt;

&lt;p&gt;
What's all the business with the &lt;code&gt;Map&amp;lt;String, String&amp;gt; configuration&lt;/code&gt;?  That's a Tapestry IoC &lt;a href="http://tapestryjava.blogspot.com/2008/09/tapestry-5-ioc-introducing-service.html"&gt;mapped configuration&lt;/a&gt;, that allows us to extend the configuration of the Kaptcha Producer ... say, to change the width or height or color scheme.

&lt;p&gt;
Note that this was my choice, to have a centralized text and image producer, so that all CAPTCHAs in the application would have a uniform look and feel. Another alterntiave would have been to have the KaptchaImage component (described shortly) have its own instance of DefaultKaptcha, with parameters to control its configuration.

&lt;h2&gt;KaptchaImage Component&lt;/h2&gt;

&lt;p&gt;
So with this service in place, how do we generate the image?  This is done in three steps:
&lt;ul&gt;
&lt;li&gt;Selecting a secret word and storing it persistently in the session
&lt;li&gt;Rendering an &amp;lt;img&amp;gt; element, including a &lt;code&gt;src&lt;/code&gt; attribute
&lt;li&gt;Providing an image byte stream when asked by the browser
&lt;/ul&gt;

&lt;pre&gt;
package com.myclient.components;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;

import javax.imageio.ImageIO;

import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.Link;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.SupportsInformalParameters;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.Response;

import com.myclient.services.kaptcha.KaptchaProducer;

/**
 * Part of a Captcha based authentication scheme; a KaptchaImage generates a new
 * text image whenever it &lt;em&gt;renders&lt;/em&gt; and can provide the previously
 * rendred text subsequently (it is stored persistently in the session).
 * 
 * The component renders an &amp;lt;img&amp;gt; tag, including width and height
 * attributes. Other attributes come from informal parameters.
 */
@SupportsInformalParameters
public class KaptchaImage {

  @Persist
  private String captchaText;

  @Inject
  private KaptchaProducer producer;

  @Inject
  private ComponentResources resources;

  @Inject
  private Response response;

  public String getCaptchaText() {
    return captchaText;
  }

  void setupRender() {
    captchaText = producer.createText();
  }

  boolean beginRender(MarkupWriter writer) {
    Link link = resources.createEventLink("image");

    writer.element("img",

    "src", link.toAbsoluteURI(),

    "width", producer.getWidth(),

    "height", producer.getHeight());

    resources.renderInformalParameters(writer);

    writer.end();

    return false;
  }

  void onImage() throws IOException {

    BufferedImage image = producer.createImage(captchaText);

    response.setDateHeader("Expires", 0);
    response.setHeader("Cache-Control",
        "no-store, no-cache, must-revalidate");
    response.setHeader("Cache-Control", "post-check=0, pre-check=0");
    response.setHeader("Pragma", "no-cache");

    OutputStream stream = response.getOutputStream("image/jpeg");

    ImageIO.write(image, "jpg", stream);

    stream.flush();

    stream.close();
  }
}
&lt;/pre&gt;

&lt;p&gt;
This component (which has no template) has two &lt;a href="http://tapestry.apache.org/tapestry5.1/guide/rendering.html"&gt;render phase methods&lt;/a&gt;.  In &lt;code&gt;setupRender()&lt;/code&gt; we choose the secret word; since the &lt;code&gt;captchaText&lt;/code&gt; field has the &lt;a href="http://tapestry.apache.org/tapestry5.1/guide/appstate.html"&gt;@Persist annotation&lt;/a&gt;, it's value will be stored in the session.

&lt;p&gt;
Inside &lt;code&gt;beginRender()&lt;/code&gt; is where we render the image. We also generate a callback link for an event named "image".  The URL Tapestry generates will identify the page and component within the page, as well as this event name.

&lt;p&gt;
Notice how we use the &lt;code&gt;getWidth()&lt;/code&gt; and &lt;code&gt;getHeight()&lt;/code&gt; extensions on the service interface to set these attributes of the &amp;lt;img&amp;gt; tag.

&lt;p&gt;
Later, the browser will send a request for the event, and the &lt;code&gt;onImage()&lt;/code&gt; event handler method will be invoked. This is where we get the image bytestream from the service and pump it down to the client. As you can see, we set a bunch of header values to ensure that the browser won't cache the image.

&lt;h2&gt;KaptchaField Component&lt;/h2&gt;

&lt;p&gt;
The last part of the overall puzzle is the text field. Again, there are two main responsibilities:
&lt;ul&gt;
&lt;li&gt;Rendering out the text field (when rendering)
&lt;li&gt;Validating that the user entered the correct secret text (when the form is submitted)
&lt;/ul&gt;

&lt;pre&gt;
package com.myclient.components;

import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.FieldValidator;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.ValidationTracker;
import org.apache.tapestry5.annotations.BeginRender;
import org.apache.tapestry5.annotations.Environmental;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.SupportsInformalParameters;
import org.apache.tapestry5.corelib.base.AbstractField;
import org.apache.tapestry5.ioc.Messages;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.FieldValidatorSource;
import org.apache.tapestry5.services.Request;

/**
 * Field paired with a {@link KaptchaImage} to ensure that the user has provided
 * the correct value.
 * 
 */
@SupportsInformalParameters
public class KaptchaField extends AbstractField {

  /**
   * The image output for this field. The image will display a distorted text
   * string. The user must decode the distorted text and enter the same value.
   */
  @Parameter(required = true, defaultPrefix = BindingConstants.COMPONENT)
  private KaptchaImage image;

  @Inject
  private Request request;

  @Inject
  private Messages messages;

  @Inject
  private ComponentResources resources;

  @Environmental
  private ValidationTracker validationTracker;

  @Inject
  private FieldValidatorSource fieldValidatorSource;

  @Override
  public boolean isRequired() {
    return true;
  }

  @BeginRender
  boolean renderTextField(MarkupWriter writer) {

    writer.element("input",

    "type", "password",

    "id", getClientId(),

    "name", getControlName(),

    "value", "");

    resources.renderInformalParameters(writer);

    FieldValidator fieldValidator = fieldValidatorSource.createValidator(
        this, "required", null);

    fieldValidator.render(writer);

    writer.end();

    return false;
  }

  @Override
  protected void processSubmission(String elementName) {

    String userValue = request.getParameter(elementName);

    if (image.getCaptchaText().equals(userValue))
      return;

    validationTracker.recordError(this, messages.get("incorrect-captcha"));
  }

}
&lt;/pre&gt;

&lt;p&gt;
The &lt;code&gt;renderTextField()&lt;/code&gt; method is largely straight forward: by the time this is invoked, the unique &lt;em&gt;clientId&lt;/em&gt; and &lt;em&gt;controlName&lt;/em&gt; will already have been set for the field. The only trick here is to create some client-side validation to enforce that the field is required.

&lt;p&gt;
Later the form will be submitted by the user and the &lt;code&gt;processSubmission()&lt;/code&gt; method is invoked. It asks the KaptchaImage for the stored text and compares it to the user's input. If invalid, then an error message is recorded, associated with the field.  The actual error text is stored in the component's message catalog.

&lt;h2&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;
Tapestry's approach is quite often about &lt;em&gt;integration&lt;/em&gt;: integration of component code with other resources (such as templates or message catalogs), integration of components with other components, and integration of components with services. Here we get to see how a singleton service can be used by any number of components, how two components can be connected together, and how easy it is to provide logic both when rendering a page &lt;em&gt;and&lt;/em&gt; on  later related requests from the client.

&lt;h2&gt;Update&lt;/h2&gt;

&lt;p&gt;
If you check the comments below, you'll see that Jon and I have been sparring good-naturedly about what constitutes "simple". Most of the code is related to &lt;em&gt;integration&lt;/em&gt;: integrating the Kaptcha code into the Tapestry infrastructure; adding features such as just-in-time initialization to the DefaultProducer code, adding new features (access to the width and height of the image), allowing for configuration of the Kaptcha Producer in the "Tapestry way" (via contribution methods in module classes), and hooking into Tapestry's normal infrastructure for handling form submissions and reporting user input errors ... even client-side logic to enforce that the field is required.

&lt;p&gt;
All that integration is what allows the end-developer to get by with just the following in their
page template:

&lt;pre&gt;
  &amp;lt;t:kaptchaimage t:id="kaptcha"/&amp;gt;
  &amp;lt;br/&amp;gt;
  &amp;lt;t:kaptchafield image="kaptcha"/&amp;gt;&lt;/pre&gt;

&lt;p&gt;
... and even that could be compressed down to a single convenience component wrapping the two underlying components:

&lt;pre&gt;
  &amp;lt;t:kaptcha/&amp;gt;
&lt;/pre&gt;

&lt;p&gt;
That is simplicity: no decisions to make, no URLs to map, no other files to edit, no additional code to write.

&lt;p&gt;
However, this still follows the Law of Immutable Complexity: making one part of a system simpler will make other parts more complex. In this situation, that extra complexity is the integration code (the two components and the service that Jon objects to). That's a trade-off I'm always willing to make: write some medium complex code once (and test it, once) and then be able to use it wherever I want.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-8487338795694540626?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/VWeFjh-63XQ" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/8487338795694540626/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=8487338795694540626" title="11 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/8487338795694540626?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/8487338795694540626?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/VWeFjh-63XQ/tapestry-and-kaptcha.html" title="Tapestry and Kaptcha" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">11</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2009/12/tapestry-and-kaptcha.html</feedburner:origLink></entry><entry gd:etag="W/&quot;C08GQ3g6eCp7ImA9WxNaGEs.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-6084730132221502095</id><published>2009-12-03T09:37:00.000-08:00</published><updated>2009-12-03T09:37:02.610-08:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-12-03T09:37:02.610-08:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="testng" /><category scheme="http://www.blogger.com/atom/ns#" term="testing" /><category scheme="http://www.blogger.com/atom/ns#" term="selenium" /><title>TestNG and Selenium</title><content type="html">&lt;p&gt;
I love working on client projects, because those help me really understand how Tapestry gets used, and the problems people are running in to. On site training is another good way to see where the theory meets (or misses) the reality.

&lt;p&gt;
In any case, I'm working for a couple of clients right now for whom testing is, rightfully, quite important. My normal approach is to write unit tests to test specific error cases (or other unusual cases), and then write integration tests to run through main use cases.  I consider this a balanced approach, that recognizes that a lot of what Tapestry does &lt;em&gt;is&lt;/em&gt; integration.

&lt;p&gt;
One of the reasons I like &lt;a href="http://testng.org/doc/"&gt;TestNG&lt;/a&gt; is that it seamlessly spans from unit tests to integration tests. All of Tapestry's internal tests (about 1500 individual tests) are written using TestNG, and
Tapestry includes a base test case class for working with &lt;a href="http://seleniumhq.org/"&gt;Selenium&lt;/a&gt;:  &lt;a href="http://tapestry.apache.org/tapestry5.1/apidocs/org/apache/tapestry5/test/AbstractIntegrationTestSuite.html"&gt;AbstractIntegrationTestSuite&lt;/a&gt;. This class does some useful things:
&lt;ul&gt;
&lt;li&gt;Launches your application using Jetty
&lt;li&gt;Launches a SeleniumServer (which drives a web browser that can exercise your application)
&lt;li&gt;Creates an instance of the Selenium client
&lt;li&gt;&lt;em&gt;Implements all the methods of Selenium&lt;/em&gt;, redirecting each to the Selenium instance
&lt;li&gt;Adds additional error reporting around any Selenium client calls that fail
&lt;/ul&gt;

&lt;p&gt;
These are all useful things, but the class has gotten a little long in the tooth ... it has a couple of critical short-comings:
&lt;ul&gt;
&lt;li&gt;It runs your application using Jetty 5 (bundled with SeleniumServer)
&lt;li&gt;It starts and stops the &lt;em&gt;stack&lt;/em&gt; (Selenium, SeleniumServer, Jetty) around each &lt;em&gt;class&lt;/em&gt;
&lt;/ul&gt;

&lt;p&gt;
For my current client, a couple of resources require JNDI, and so I'm using Jetty 7 to run the application (at least in development, and possibly in deployment as well). Fortunately, Jetty 5 uses the old &lt;code&gt;org.mortbay.jetty&lt;/code&gt; packages, and Jetty 7 uses the new &lt;code&gt;org.eclipse.jetty&lt;/code&gt; packages, so both versions of the server can co-exist within the same application.

&lt;p&gt;
The larger problem is that I didn't want a single titanic test case for my entire application; I wanted to break it up in other ways, by Tapestry page initially.

&lt;p&gt;
I could create additional subclasses of AbstractIntegrationTestSuite, but then the tests will spend a huge amount of time starting and stopping Firefox and friends. I really want that stuff to start just &lt;em&gt;once&lt;/em&gt;.

&lt;p&gt;
What I've done is a bit of refactoring, by leveraging some features of TestNG that I hadn't previously used.

&lt;p&gt;
The part of AbstractIntegrationTestSuite responsible for starting and stopping the &lt;em&gt;stack&lt;/em&gt; is broken out into its own class.  This new class, SeleniumLauncher, is responsible for starting and stopping the stack around an entire TestNG &lt;em&gt;test&lt;/em&gt;.  In the TestNG terminology, a &lt;em&gt;suite&lt;/em&gt; contains multiple &lt;em&gt;tests&lt;/em&gt;, and a &lt;em&gt;test&lt;/em&gt; contains test cases (found in individual classes, within scanned packages).  The &lt;em&gt;test case&lt;/em&gt; contains test and configuration methods.

&lt;p&gt;Here's what I've come up with:
&lt;pre&gt;
package com.myclient.itest;

import org.apache.tapestry5.test.ErrorReportingCommandProcessor;
import org.eclipse.jetty.server.Server;
import org.openqa.selenium.server.RemoteControlConfiguration;
import org.openqa.selenium.server.SeleniumServer;
import org.testng.ITestContext;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeTest;

import com.myclient.RunJetty;
import com.thoughtworks.selenium.CommandProcessor;
import com.thoughtworks.selenium.DefaultSelenium;
import com.thoughtworks.selenium.HttpCommandProcessor;
import com.thoughtworks.selenium.Selenium;

public class SeleniumLauncher {

  public static final String SELENIUM_KEY = "myclient.selenium";

  public static final String BASE_URL_KEY = "myclient.base-url";

  public static final int JETTY_PORT = 9999;

  public static final String BROWSER_COMMAND = "*firefox";

  private Selenium selenium;

  private Server jettyServer;

  private SeleniumServer seleniumServer;

  /** Starts the SeleniumServer, the application, and the Selenium instance. */
  @BeforeTest(alwaysRun = true)
  public void setup(ITestContext context) throws Exception {

    jettyServer = RunJetty.start(JETTY_PORT);

    seleniumServer = new SeleniumServer();

    seleniumServer.start();

    String baseURL = String.format("http://localhost:%d/", JETTY_PORT);

    CommandProcessor cp = new HttpCommandProcessor("localhost",
        RemoteControlConfiguration.DEFAULT_PORT, BROWSER_COMMAND,
        baseURL);

    selenium = new DefaultSelenium(new ErrorReportingCommandProcessor(cp));

    selenium.start();

    context.setAttribute(SELENIUM_KEY, selenium);
    context.setAttribute(BASE_URL_KEY, baseURL);
  }

  /** Shuts everything down. */
  @AfterTest(alwaysRun = true)
  public void cleanup() throws Exception {
    if (selenium != null) {
      selenium.stop();
      selenium = null;
    }

    if (seleniumServer != null) {
      seleniumServer.stop();
      seleniumServer = null;
    }

    if (jettyServer != null) {
      jettyServer.stop();
      jettyServer = null;
    }
  }
}

&lt;/pre&gt;

&lt;p&gt;
Notice that we're using the &lt;code&gt;@BeforeTest&lt;/code&gt; and &lt;code&gt;@AfterTest&lt;/code&gt; annotations; that means any number of tests cases can execute using the same stack. The stack is only started once.

&lt;p&gt;
Also, notice how we're using the &lt;a href="http://testng.org/javadocs/org/testng/ITestContext.html"&gt;ITestContext&lt;/a&gt; to communicate information to the tests in the form of attributes. TestNG has a built in form of dependency injection; any method that needs the ITestContext can get it just by declaring a parameter of that type.

&lt;p&gt;AbstractIntegrationTestSuite2 is the new base class for writing integration tests:

&lt;pre&gt;
package com.myclient.itest;

import java.lang.reflect.Method;

import org.apache.tapestry5.test.AbstractIntegrationTestSuite;
import org.apache.tapestry5.test.RandomDataSource;
import org.testng.Assert;
import org.testng.ITestContext;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;

import com.mchange.util.AssertException;
import com.thoughtworks.selenium.Selenium;

public abstract class AbstractIntegrationTestSuite2 extends Assert implements
    Selenium {

  public static final String BROWSERBOT = "selenium.browserbot.getCurrentWindow()";

  public static final String SUBMIT = "//input[@type='submit']";

  /**
   * 15 seconds
   */
  public static final String PAGE_LOAD_TIMEOUT = "15000";

  private Selenium selenium;

  private String baseURL;

  protected String getBaseURL() {
    return baseURL;
  }

  @BeforeClass
  public void setup(ITestContext context) {
    selenium = (Selenium) context
        .getAttribute(SeleniumLauncher.SELENIUM_KEY);
    baseURL = (String) context.getAttribute(SeleniumLauncher.BASE_URL_KEY);
  }

  @AfterClass
  public void cleanup() {
    selenium = null;
    baseURL = null;
  }

  @BeforeMethod
  public void indicateTestMethodName(Method testMethod) {
    selenium.setContext(String.format("Running %s: %s", testMethod
        .getDeclaringClass().getSimpleName(), testMethod.getName()
        .replace("_", " ")));
  }

  /* Start of delegate methods */
  public void addCustomRequestHeader(String key, String value) {
    selenium.addCustomRequestHeader(key, value);
  }

  ...
}
&lt;/pre&gt;

&lt;p&gt;
Inside the &lt;code&gt;@BeforeClass&lt;/code&gt;-annotated method, we receive the test context and extract the selenium instance and base URL put in there by SeleniumLauncher.

&lt;p&gt;
The last piece of the puzzle is the code that launches Jetty. Normally, I test my web applications using the Eclipse &lt;a href="http://code.google.com/p/run-jetty-run/"&gt;run-jetty-run&lt;/a&gt; plugin, but RJR doesn't support the "Jetty Plus" functionality, including JNDI. Thus I've created an application to run Jetty embedded:

&lt;pre&gt;
package com.myclient;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;

public class RunJetty {

  public static void main(String[] args) throws Exception {

    start().join();
  }

  public static Server start() throws Exception {
    return start(8080);
  }

  public static Server start(int port) throws Exception {
    Server server = new Server(port);

    WebAppContext webapp = new WebAppContext();
    webapp.setContextPath("/");
    webapp.setWar("src/main/webapp");

    // Note: Need jetty-plus and jetty-jndi on the classpath; otherwise
    // jetty-web.xml (where datasources are configured) will not be
    // read.

    server.setHandler(webapp);

    server.start();

    return server;
  }
}
&lt;/pre&gt;


&lt;p&gt;
This is all looking great. I expect to move this code into Tapestry 5.2 pretty soon. What I'm puzzling on is a couple of extra ideas:
&lt;ul&gt;
&lt;li&gt;Better flexibility on starting up Jetty so that you can hook your own custom Jetty server configuration in.
&lt;li&gt;Ability to run multiple browser agents, so that a single test suite can execute against Internet Explorer, Firefox, Safari, etc.  In many cases, the same test method might be invoked multiple times, to test against different browsers.
&lt;/ul&gt;

&lt;p&gt;
Anyway, this is just one of a number of very cool ideas I expect to roll into Tapestry 5.2 in the near future.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-6084730132221502095?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/a_eXPZLq6uQ" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/6084730132221502095/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=6084730132221502095" title="4 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/6084730132221502095?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/6084730132221502095?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/a_eXPZLq6uQ/testng-and-selenium.html" title="TestNG and Selenium" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">4</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2009/12/testng-and-selenium.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DUQGR3k6fip7ImA9WxNbGU0.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-2768185847573524355</id><published>2009-11-22T08:27:00.000-08:00</published><updated>2009-11-22T08:28:46.716-08:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-11-22T08:28:46.716-08:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="devoxx" /><title>Fun and Jet Lag at Devoxx 2009</title><content type="html">&lt;p&gt;Last week was a bit of a blur, a mix of &lt;em&gt;severe&lt;/em&gt; jet lag, a bit of over-indulgence at the speaker's dinner, and the thrill of presenting on Tapestry and on Clojure in a rock-star environment.  &lt;a href="http://www.devoxx.com/display/DV09/Home"&gt;Devoxx&lt;/a&gt; is well known for the venue ... speaking in front of a three story screen to a crowd numbering in the hundreds is a bit different that chatting to a group in front of a whiteboard, I can say.

&lt;p&gt;
If you were at the show, please take a minute to rate my sessions:

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://speakerrate.com/talks/1856-clojure-functional-concurrency-for-the-jvm"&gt;Clojure: Functional Concurrency for the JVM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://speakerrate.com/talks/1857-tapestry-5-java-power-scripting-ease"&gt;Tapestry 5: Java Power, Scripting Ease&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-2768185847573524355?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/a0hPCSjob_Y" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/2768185847573524355/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=2768185847573524355" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/2768185847573524355?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/2768185847573524355?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/a0hPCSjob_Y/fun-and-jet-lag-at-devoxx-2009.html" title="Fun and Jet Lag at Devoxx 2009" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2009/11/fun-and-jet-lag-at-devoxx-2009.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DUMCSX49fCp7ImA9WxNUFkw.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-4239494976194943574</id><published>2009-11-07T10:11:00.000-08:00</published><updated>2009-11-07T10:11:08.064-08:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-11-07T10:11:08.064-08:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="tapestry" /><title>Next Steps for Tapestry</title><content type="html">&lt;p&gt;I've been consciously letting Tapestry 5.1 sit and stabilize for a while ... a time that's stretched a few months longer than I initially intended.

&lt;p&gt;
This is due to a number of factors: my return to independent consulting, my desire to write a definitive Tapestry 5 book, and preparations for many trips and speaking engagements.

&lt;p&gt;
All of these factors have worked on each other: I've been improving and extending my Tapestry Workshop training materials which can be quite time consuming.  I've also (over the last several months) been on the road several times, talking about Tapestry or doing Tapestry training.

&lt;p&gt;
I do want to write a book on Tapestry but if I start writing 5.2 code, I know I'll be sucked right in ... lots of code (that darn Spring Web Flow integration for sure this time) and bug fixes. 

&lt;p&gt;In addition, I've had an embarassment of riches: two main clients, one regular part time, and the other requesting (but not always getting) all my remaining time. I also have additional clients and training engagements waiting in the wings. I simply have a lot of draws on my time.

&lt;p&gt;
As usual, working on real-world projects lets me experience the "rough edges" of Tapestry and fills me with ideas on how to address those in the next release ... often by splitting up Tapestry services into smaller, more easily overridden chunks and carefully moving internal services out into the public APIs.

&lt;p&gt;Finally, I've been very pleased by the fact that as I've stepped back temporarily from my normal  stream of commits, the other Tapestry developers have stepped in and filled the gap. There's been quite a bit of activity especially from Igor that I've barely had a chance to keep up on.

&lt;p&gt;
So the question is: do I wait and see if time opens up in Q1 to actually start on a T5 book ... or do I jump into 5.2 coding and leave books to others? It's much, much easier to write code than to write a book ... a book is a large amount of concentrated effort. It's very hard to accomplish anything on a book using an hour here or an evening there ... whereas Tapestry's code base lends itself to that kind of effort quite nicely.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-4239494976194943574?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/JKv2D22j1KQ" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/4239494976194943574/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=4239494976194943574" title="27 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/4239494976194943574?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/4239494976194943574?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/JKv2D22j1KQ/next-steps-for-tapestry.html" title="Next Steps for Tapestry" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">27</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2009/11/next-steps-for-tapestry.html</feedburner:origLink></entry><entry gd:etag="W/&quot;AkcCRn09fip7ImA9WxNUF00.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-8227344758020164549</id><published>2009-11-07T09:53:00.000-08:00</published><updated>2009-11-08T11:21:07.366-08:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-11-08T11:21:07.366-08:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="clojure" /><category scheme="http://www.blogger.com/atom/ns#" term="cascade" /><title>Progress on Cascade</title><content type="html">&lt;p&gt;
Meanwhile, in spare minutes (and during sessions at ApacheCon), I've been continuing to work on &lt;a href="http://github.com/hlship/cascade"&gt;Cascade&lt;/a&gt;.  It's been a great learning exercise for me, pushing my understanding of both &lt;a href="http://clojure.org"&gt;Clojure&lt;/a&gt; and functional programming in general ... and especially, some pretty advanced meta-programming with macros.

&lt;p&gt;
I'm also using Cascade as a kind of test bed for ideas that will eventually appear in Tapestry.

&lt;p&gt;
Not everything turns out exactly as I've planned. For example, I've been very excited about &lt;em&gt;invariants&lt;/em&gt;, portions of the rendered DOM that could be cached from one request to another, to speed up the rendering. Like Tapestry, Cascade views render a DOM structure which can be manipulated (in an intermediate stage) before being streamed as text. This is a useful and powerful concept in a number of ways.

&lt;p&gt;
My thinking has been that a typical view will contain sections of the template that are invariant: unchanging, and that there would be a benefit to building that sub-section of the DOM once and reusing it efficiently in later renderings of the view.

&lt;p&gt;
Clojure template forms are processed by macros to become normal Clojure code.  Thus something like &lt;code&gt;(template :p [ "Hello" ])&lt;/code&gt; will be transformed into code, approximately &lt;code&gt;(element-node :p nil (combine (text-node "Hello")))&lt;/code&gt;.  My approach was to tag the new Clojure code forms (the list consisting of &lt;code&gt;element-node&lt;/code&gt;, &lt;code&gt;:p&lt;/code&gt;, etc.) with meta data to identify it as invariant. Eventually this would propagate up to a higher level and code to manage a cache would be wrapped around it:  &lt;code&gt;(or (read-view-cache 97) (update-view-cache 97 (element-node :p ...&lt;/code&gt;

&lt;p&gt;
Fun stuff ... until I put it into practice (after a fair amount of debugging) and discovered that in the views I've created so far (for testing purposes), the number of nodes that can be cached is low; any use  of a symbol or a function call mixed into the template "taints" it as variant. I wasn't set up to do performance measurements, but my gut feeling is that the overhead of managing the caches would overshadow the savings from the small islands of reused DOM nodes.

&lt;p&gt;
Back to Cascade as a learning experience: just because this didn't work out doesn't mean I didn't learn a lot from heading down that direction, and certainly the amount of code it took was quite small. I have it &lt;a href="http://github.com/hlship/cascade/tree/optimize-render-tree"&gt;stored in a branch&lt;/a&gt; in case I ever want to give it another shot.

&lt;p&gt;
I will have all the basic features of Cascade implemented pretty soon; I'm looking forward to seeing what the larger Clojure community makes of it. In the meantime, it has served as a great way for me to dig deep into Clojure ... I'll be putting together more sessions for &lt;a href="http://nofluffjuststuff.com"&gt;NoFluffJustStuff&lt;/a&gt; and more articles for the &lt;a href="http://www.nofluffjuststuff.com/home/magazine_subscribe"&gt;companion magazine&lt;/a&gt; based on all this.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-8227344758020164549?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/8HdjJOUN03k" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/8227344758020164549/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=8227344758020164549" title="1 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/8227344758020164549?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/8227344758020164549?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/8HdjJOUN03k/progress-on-cascade.html" title="Progress on Cascade" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">1</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2009/11/progress-on-cascade.html</feedburner:origLink></entry><entry gd:etag="W/&quot;D0MBR3w_eip7ImA9WxNUFkw.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-3595413176312725928</id><published>2009-11-07T09:37:00.000-08:00</published><updated>2009-11-07T09:37:36.242-08:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-11-07T09:37:36.242-08:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="tapestry" /><category scheme="http://www.blogger.com/atom/ns#" term="javascript" /><category scheme="http://www.blogger.com/atom/ns#" term="extjs" /><title>Rethinking Tapestry's approach to JavaScript</title><content type="html">&lt;p&gt;
I've been doing a lot of work for a client using the &lt;a href="http://www.extjs.com/"&gt;ExtJS&lt;/a&gt; library and that, combined with many other things I've been looking at, has started to shift my attitude to the correct approach to client-side JavaScript.  Not a sea change, just a minor adjustment.

&lt;p&gt;
Until now, I've had the approach that the page should popup "complete" and that the JavaScript should only manipulate (or add to) the content already present.  In most cases, such as adding validations to user input fields, that works fine: you write out the HTML for the field, remember the &lt;code&gt;id&lt;/code&gt; for the field, then add JavaScript that adds event handlers to the field, finding it by its id.

&lt;p&gt;
However, for more complex cases, such as &lt;a href="http://tapestry.apache.org/tapestry5.1/tapestry-core/ref/org/apache/tapestry5/corelib/components/Palette.html"&gt;Tapestry's Palette component&lt;/a&gt;, I've been coding like its 2001 for too long.

&lt;p&gt;The Palette component renders out two &amp;lt;select&amp;gt; elements, plus a number of &amp;lt;divs&amp;gt;, a few buttons, and a chunk of JavaScript to connect it all together. This means generating a &lt;em&gt;lot&lt;/em&gt; of related ids on the server (to match against the generated HTML markup) and passing those down to the client.

&lt;p&gt;
It's effective but it reflects my relative naivete with JavaScript back in &lt;a href="http://onjava.com/onjava/2001/11/21/tapestry.html"&gt;2001&lt;/a&gt;. It's now so much easier to create a DOM structure on the client, using any of the major JavaScript libraries.  That, in turn, makes it much more reasonable to just write out an empty &amp;lt;div&amp;gt; element with an id and &lt;em&gt;install&lt;/em&gt; into that empty space all the markup for the component, generated on the client side.  In fact, I envision a version of the Palette for Tapestry 5.2 that starts as a &amp;lt;select&amp;gt; configured for multiple selection and is converted on the fly to the familiar Palette component entirely on the client side ... which means that it could actually operate in a functional, if limited way, even if JavaScript is disabled in the client.

&lt;p&gt;
ExtJS includes a number of other ideas that are worth thinking about; I've been very impressed by the concept of a separate controller hierarchy on the client side (ExtJS calls these "components" as differentiated from the DOM nodes they create and manage). That's something that's ad-hoc and inconsistent in Tapestry's client-side library. I think embracing a similar, more structured approach could make it easier for Tapestry to embrace even more dynamic Ajax behavior.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-3595413176312725928?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/zJV0NHDI-d4" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/3595413176312725928/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=3595413176312725928" title="2 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/3595413176312725928?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/3595413176312725928?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/zJV0NHDI-d4/rethinking-tapestrys-approach-to.html" title="Rethinking Tapestry's approach to JavaScript" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">2</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2009/11/rethinking-tapestrys-approach-to.html</feedburner:origLink></entry><entry gd:etag="W/&quot;A08MRn06fyp7ImA9WxNUFE4.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-4154557899332868607</id><published>2009-11-05T08:51:00.001-08:00</published><updated>2009-11-05T08:51:27.317-08:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-11-05T08:51:27.317-08:00</app:edited><title>Tapestry 5: Java Power, Scripting Ease (ApacheCon 2009)</title><content type="html">I've uploaded my presentation from ApacheCon 2009. Of coure, the fun parts are the embedded screen casts, and you need to see me live to get that part!&lt;div style="width:425px;text-align:left" id="__ss_2430037"&gt;&lt;a style="font:14px Helvetica,Arial,Sans-serif;display:block;margin:12px 0 3px 0;text-decoration:underline;" href="http://www.slideshare.net/hlship/tapestry-5-java-power-scripting-ease" title="Tapestry 5: Java Power, Scripting Ease"&gt;Tapestry 5: Java Power, Scripting Ease&lt;/a&gt;&lt;object style="margin:0px" width="425" height="355"&gt;&lt;param name="movie" value="http://static.slidesharecdn.com/swf/ssplayer2.swf?doc=apachecon-2009-tapestry-key-091105094222-phpapp02&amp;stripped_title=tapestry-5-java-power-scripting-ease" /&gt;&lt;param name="allowFullScreen" value="true"/&gt;&lt;param name="allowScriptAccess" value="always"/&gt;&lt;embed src="http://static.slidesharecdn.com/swf/ssplayer2.swf?doc=apachecon-2009-tapestry-key-091105094222-phpapp02&amp;stripped_title=tapestry-5-java-power-scripting-ease" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="425" height="355"&gt;&lt;/embed&gt;&lt;/object&gt;&lt;div style="font-size:11px;font-family:tahoma,arial;height:26px;padding-top:2px;"&gt;View more &lt;a style="text-decoration:underline;" href="http://www.slideshare.net/"&gt;documents&lt;/a&gt; from &lt;a style="text-decoration:underline;" href="http://www.slideshare.net/hlship"&gt;Howard Lewis Ship&lt;/a&gt;.&lt;/div&gt;&lt;/div&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-4154557899332868607?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/kCcCLRm1coc" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/4154557899332868607/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=4154557899332868607" title="2 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/4154557899332868607?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/4154557899332868607?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/kCcCLRm1coc/tapestry-5-java-power-scripting-ease.html" title="Tapestry 5: Java Power, Scripting Ease (ApacheCon 2009)" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">2</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2009/11/tapestry-5-java-power-scripting-ease.html</feedburner:origLink></entry><entry gd:etag="W/&quot;D0AGQng5cSp7ImA9WxNUEE4.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-4354102977423292097</id><published>2009-10-31T17:34:00.000-07:00</published><updated>2009-10-31T17:35:23.629-07:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-10-31T17:35:23.629-07:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="ant" /><category scheme="http://www.blogger.com/atom/ns#" term="maven" /><title>Maven: Throwing out the bath water, keeping the baby</title><content type="html">&lt;p&gt;
... in other words, a first step towards using &lt;a href="http://maven.apache.org"&gt;Maven&lt;/a&gt; for dependency management but NOT builds. That's the irony of Maven ... they've conflated two things (dependency management and builds) in such as way that they make the useful one (dependency management) painful because the build system is so awful.

&lt;p&gt;
As an interrum step between full Maven and (most likely) &lt;a href="http://www.gradle.org/"&gt;Gradle&lt;/a&gt;, I've been looking at a way to use Maven for dependencies only in a way that is compatible with Eclipse ... without using the often flakey and undependable &lt;a href="http://m2eclipse.sonatype.org/"&gt;M2Eclipse&lt;/a&gt; plugin.

&lt;p&gt;
In any case, rather than assuming that dependencies might change at &lt;em&gt;any point in time at all&lt;/em&gt;, let's assume that when I change dependencies (by manually editing pom.xml) I know it and am willing to run a command to bring Eclipse (and my Ant-based build) in line.

&lt;p&gt;
First, my pom.xml:

&lt;pre&gt;
&amp;lt;project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
  xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"&amp;gt;
  &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;
  &amp;lt;groupId&amp;gt;org.example&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;myapp&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
  &amp;lt;dependencies&amp;gt;
    &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;org.apache.tapestry&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;tapestry-hibernate&amp;lt;/artifactId&amp;gt;
      &amp;lt;version&amp;gt;${tapestry-version}&amp;lt;/version&amp;gt;
      &amp;lt;scope&amp;gt;compile&amp;lt;/scope&amp;gt;
      &amp;lt;exclusions&amp;gt;
        &amp;lt;exclusion&amp;gt;
          &amp;lt;artifactId&amp;gt;log4j&amp;lt;/artifactId&amp;gt;
          &amp;lt;groupId&amp;gt;log4j&amp;lt;/groupId&amp;gt;
        &amp;lt;/exclusion&amp;gt;
        &amp;lt;exclusion&amp;gt;
          &amp;lt;artifactId&amp;gt;slf4j-log4j12&amp;lt;/artifactId&amp;gt;
          &amp;lt;groupId&amp;gt;org.slf4j&amp;lt;/groupId&amp;gt;
        &amp;lt;/exclusion&amp;gt;
      &amp;lt;/exclusions&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;org.apache.tapestry&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;tapestry-test&amp;lt;/artifactId&amp;gt;
      &amp;lt;version&amp;gt;5.2.0-SNAPSHOT&amp;lt;/version&amp;gt;
      &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;javax.servlet&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;servlet-api&amp;lt;/artifactId&amp;gt;
      &amp;lt;version&amp;gt;2.4&amp;lt;/version&amp;gt;
      &amp;lt;scope&amp;gt;provided&amp;lt;/scope&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;org.apache.lucene&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;lucene-core&amp;lt;/artifactId&amp;gt;
      &amp;lt;version&amp;gt;2.4.1&amp;lt;/version&amp;gt;
      &amp;lt;scope&amp;gt;compile&amp;lt;/scope&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;org.hibernate&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;hibernate-search&amp;lt;/artifactId&amp;gt;
      &amp;lt;version&amp;gt;3.1.1.GA&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;commons-lang&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;commons-lang&amp;lt;/artifactId&amp;gt;
      &amp;lt;version&amp;gt;2.4&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;commons-beanutils&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;commons-beanutils&amp;lt;/artifactId&amp;gt;
      &amp;lt;version&amp;gt;1.8.0&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;ch.qos.logback&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;logback-classic&amp;lt;/artifactId&amp;gt;
      &amp;lt;version&amp;gt;0.9.17&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;org.slf4j&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;slf4j-api&amp;lt;/artifactId&amp;gt;
      &amp;lt;version&amp;gt;1.5.8&amp;lt;/version&amp;gt;
      &amp;lt;type&amp;gt;jar&amp;lt;/type&amp;gt;
      &amp;lt;scope&amp;gt;compile&amp;lt;/scope&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;postgresql&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;postgresql&amp;lt;/artifactId&amp;gt;
      &amp;lt;version&amp;gt;8.4-701.jdbc4&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;xerces&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;xercesImpl&amp;lt;/artifactId&amp;gt;
      &amp;lt;version&amp;gt;2.4.0&amp;lt;/version&amp;gt;
      &amp;lt;type&amp;gt;jar&amp;lt;/type&amp;gt;
      &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;com.howardlewisship&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;tapx-datefield&amp;lt;/artifactId&amp;gt;
      &amp;lt;version&amp;gt;${tapx-version}&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;com.howardlewisship&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;tapx-prototype&amp;lt;/artifactId&amp;gt;
      &amp;lt;version&amp;gt;${tapx-version}&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;org.testng&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;testng&amp;lt;/artifactId&amp;gt;
      &amp;lt;version&amp;gt;5.9&amp;lt;/version&amp;gt;
      &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
    &amp;lt;/dependency&amp;gt;
  &amp;lt;/dependencies&amp;gt;
  &amp;lt;repositories&amp;gt;
    &amp;lt;repository&amp;gt;
      &amp;lt;id&amp;gt;tapestry360-snapshots&amp;lt;/id&amp;gt;
      &amp;lt;url&amp;gt;http://tapestry.formos.com/maven-snapshot-repository/&amp;lt;/url&amp;gt;
    &amp;lt;/repository&amp;gt;
    &amp;lt;repository&amp;gt;
      &amp;lt;id&amp;gt;repository.jboss.org&amp;lt;/id&amp;gt;
      &amp;lt;url&amp;gt;http://repository.jboss.org/maven2/&amp;lt;/url&amp;gt;
    &amp;lt;/repository&amp;gt;
  &amp;lt;/repositories&amp;gt;
  &amp;lt;properties&amp;gt;
    &amp;lt;tapestry-version&amp;gt;5.1.0.5&amp;lt;/tapestry-version&amp;gt;
    &amp;lt;lucene-version&amp;gt;2.4.1&amp;lt;/lucene-version&amp;gt;
    &amp;lt;tapx-version&amp;gt;1.0.0-SNAPSHOT&amp;lt;/tapx-version&amp;gt;
  &amp;lt;/properties&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/pre&gt;

&lt;p&gt;(This was adapated from one of my client's POMs).

&lt;p&gt;Next, an Ant build file that compiles this, runs tests and builds a WAR:

&lt;pre&gt;
&amp;lt;project name="example" xmlns:mvn="urn:maven-artifact-ant"&amp;gt;

  &amp;lt;property name="classes.dir" value="target/classes" /&amp;gt;
  &amp;lt;property name="test.classes.dir" value="target/test-classes" /&amp;gt;
  &amp;lt;property name="web.lib.dir" value="target/web-libs" /&amp;gt;
  &amp;lt;property name="webapp.dir" value="src/main/webapp" /&amp;gt;
  &amp;lt;property name="webinf.dir" value="${webapp.dir}/WEB-INF" /&amp;gt;

  &amp;lt;path id="compile.path"&amp;gt;
    &amp;lt;fileset dir="lib/provided" includes="*.jar"/&amp;gt;
    &amp;lt;fileset dir="lib/runtime" includes="*.jar" /&amp;gt;
  &amp;lt;/path&amp;gt;

  &amp;lt;path id="test.path"&amp;gt;
    &amp;lt;path refid="compile.path" /&amp;gt;
    &amp;lt;pathelement path="${classes.dir}" /&amp;gt;
    &amp;lt;fileset dir="lib/test" includes="*.jar" /&amp;gt;
  &amp;lt;/path&amp;gt;

  &amp;lt;target name="clean" description="Delete all derived files."&amp;gt;
    &amp;lt;delete dir="target" quiet="true" /&amp;gt;
  &amp;lt;/target&amp;gt;

  &amp;lt;!-- Assumes that Maven's Ant library is installed in ${ANT_HOME}/lib/ext. --&amp;gt;

  &amp;lt;target name="-setup-maven"&amp;gt;
    &amp;lt;typedef resource="org/apache/maven/artifact/ant/antlib.xml" uri="urn:maven-artifact-ant" /&amp;gt;
    &amp;lt;mvn:pom id="pom" file="pom.xml" /&amp;gt;
  &amp;lt;/target&amp;gt;

  &amp;lt;macrodef name="copy-libs"&amp;gt;
    &amp;lt;attribute name="filesetrefid" /&amp;gt;
    &amp;lt;attribute name="todir" /&amp;gt;
    &amp;lt;sequential&amp;gt;
      &amp;lt;mkdir dir="@{todir}" /&amp;gt;
      &amp;lt;copy todir="@{todir}"&amp;gt;
        &amp;lt;fileset refid="@{filesetrefid}" /&amp;gt;
        &amp;lt;mapper type="flatten" /&amp;gt;
      &amp;lt;/copy&amp;gt;
    &amp;lt;/sequential&amp;gt;
  &amp;lt;/macrodef&amp;gt;

  &amp;lt;macrodef name="rebuild-lib"&amp;gt;
    &amp;lt;attribute name="base" /&amp;gt;
    &amp;lt;attribute name="scope" /&amp;gt;
    &amp;lt;attribute name="libs.id" default="@{base}.libs" /&amp;gt;
    &amp;lt;attribute name="src.id" default="@{base}.src" /&amp;gt;
    &amp;lt;sequential&amp;gt;
      &amp;lt;mvn:dependencies pomrefid="pom" filesetid="@{libs.id}" sourcesFilesetid="@{src.id}" scopes="@{scope}" /&amp;gt;
      &amp;lt;copy-libs filesetrefid="@{libs.id}" todir="lib/@{base}" /&amp;gt;
      &amp;lt;copy-libs filesetrefid="@{src.id}" todir="lib/@{base}-src" /&amp;gt;
    &amp;lt;/sequential&amp;gt;
  &amp;lt;/macrodef&amp;gt;

  &amp;lt;target name="refresh-libraries" depends="-setup-maven" description="Downloads runtime and test libraries as per POM."&amp;gt;
    &amp;lt;delete dir="lib" quiet="true" /&amp;gt;
    &amp;lt;rebuild-lib base="provided" scope="provided"/&amp;gt;
    &amp;lt;rebuild-lib base="runtime" scope="runtime,compile" /&amp;gt;
    &amp;lt;rebuild-lib base="test" scope="test" /&amp;gt;
    &amp;lt;echo&amp;gt;
      
*** Use the rebuild-classpath command to update the Eclipse .classpath file.&amp;lt;/echo&amp;gt;
  &amp;lt;/target&amp;gt;

  &amp;lt;target name="compile" description="Compile main source code."&amp;gt;
    &amp;lt;mkdir dir="${classes.dir}" /&amp;gt;
    &amp;lt;javac srcdir="src/main/java" destdir="${classes.dir}" debug="true" debuglevel="lines,vars,source"&amp;gt;
      &amp;lt;classpath refid="compile.path" /&amp;gt;
    &amp;lt;/javac&amp;gt;
  &amp;lt;/target&amp;gt;

  &amp;lt;target name="compile-tests" depends="compile" description="Compile test sources."&amp;gt;
    &amp;lt;mkdir dir="${test.classes.dir}" /&amp;gt;
    &amp;lt;javac srcdir="src/test/java" destdir="${test.classes.dir}" debug="true" debuglevel="lines,vars,source"&amp;gt;
      &amp;lt;classpath refid="test.path" /&amp;gt;
    &amp;lt;/javac&amp;gt;
  &amp;lt;/target&amp;gt;

  &amp;lt;target name="run-tests" depends="compile-tests" description="Run unit and integration tests."&amp;gt;
    &amp;lt;taskdef resource="testngtasks" classpathref="test.path" /&amp;gt;
    &amp;lt;testng haltonfailure="true"&amp;gt;
      &amp;lt;classpath&amp;gt;
        &amp;lt;path refid="test.path" /&amp;gt;
        &amp;lt;pathelement path="${test.classes.dir}" /&amp;gt;
      &amp;lt;/classpath&amp;gt;

      &amp;lt;xmlfileset dir="src/test/conf" includes="testng.xml" /&amp;gt;

    &amp;lt;/testng&amp;gt;
  &amp;lt;/target&amp;gt;


  &amp;lt;target name="war" depends="run-tests,-setup-maven" description="Assemble WAR file."&amp;gt;

    &amp;lt;!-- Copy and flatten the libraries ready for packaging. --&amp;gt;

    &amp;lt;mkdir dir="${web.lib.dir}" /&amp;gt;
    &amp;lt;copy todir="${web.lib.dir}" flatten="true"&amp;gt;
      &amp;lt;fileset dir="lib/runtime" /&amp;gt;
    &amp;lt;/copy&amp;gt;
    &amp;lt;jar destfile="${web.lib.dir}/${pom.artifactId}-${pom.version}.jar" index="true"&amp;gt;
      &amp;lt;fileset dir="src/main/resources" /&amp;gt;
      &amp;lt;fileset dir="${classes.dir}" /&amp;gt;
    &amp;lt;/jar&amp;gt;

    &amp;lt;war destfile="target/${pom.artifactId}-${pom.version}.war"&amp;gt;
      &amp;lt;fileset dir="${webapp.dir}" /&amp;gt;
      &amp;lt;lib dir="${web.lib.dir}" /&amp;gt;
    &amp;lt;/war&amp;gt;
  &amp;lt;/target&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/pre&gt;


&lt;p&gt;The key target here is &lt;code&gt;refresh-libraries&lt;/code&gt;, which deletes the lib directory then repopulates it.  It creates a sub folder for each scope (lib/provided, lib/runtime, lib/test) and another sub folder for source JARs (lib/provided-src, lib/runtime-src, etc.).

&lt;p&gt;
So how does this help Eclipse?  Ruby to the rescue:

&lt;pre&gt;
#!/usr/bin/ruby
# Rebuild the .classpath file based on the contents of lib/runtime, etc.

# Probably easier using XML Generator but don't have the docs handy

def process_scope(f, scope)
 # Now find the actual JARs and add an entry for each one.
 
 dir = "lib/#{scope}"

 return unless File.exists?(dir)
 
 Dir.entries(dir).select { |name| name =~ /\.jar$/ }.sort.each do |name|
   f.write %{  &amp;lt;classpathentry kind="lib" path="#{dir}/#{name}"}
   
   srcname = dir + "-src/" + name.gsub(/\.jar$/, "-sources.jar") 

   if File.exist?(srcname)
      f.write %{ sourcepath="#{srcname}"}
   end
   
   f.write %{/&amp;gt;\n}
 end
end

File.open(".classpath", "w") do |f|
  f.write %{&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;
&amp;lt;classpath&amp;gt;
  &amp;lt;classpathentry kind="src" path="src/main/java"/&amp;gt;
  &amp;lt;classpathentry kind="lib" path="src/main/resources"/&amp;gt;
  &amp;lt;classpathentry kind="src" path="src/test/java"/&amp;gt;
  &amp;lt;classpathentry kind="lib" path="src/test/resources"/&amp;gt;
  &amp;lt;classpathentry kind="output" path="target/classes"/&amp;gt;
  &amp;lt;classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/&amp;gt;
}

 process_scope(f, "provided")
 process_scope(f, "runtime")
 process_scope(f, "test")
 
 f.write %{
&amp;lt;/classpath&amp;gt;
}
    
end
&lt;/pre&gt;

&lt;p&gt;
That's pretty good for half an hour's work. This used to be much more difficult (in Maven 2.0.9), but the new &lt;code&gt;scopes&lt;/code&gt; attribute on the Maven &lt;code&gt;dependencies&lt;/code&gt; task makes all the difference.

&lt;p&gt;
Using this you are left with a choice: either you don't check in .classpath and the contents of the lib folder, in which case you need to execute the target and script in order to be functional ... or you simply check everything in.  I'm using &lt;a href="http://github.com"&gt;GitHub&lt;/a&gt; for my project repository ... the extra space for a few MB of libraries is not an issue and ensures that I can set up the exact classpath needed by the other developers on the project with none of the usual Maven guess-work.  I'm looking forward to never having to say "But it works for me?" or "What version of &lt;em&gt;just about anything&lt;/em&gt; do you have installed?" or "Try a clean build, and close and reopen your project, and remove and add Maven dependency support, then sacrifice a small goat" every again.

&lt;p&gt;
Next up?  Packaging most of this into an Ant library so that I can easily reuse it across projects ... or taking the time to learn Gradle and let it handle most of this distracting garbage.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-4354102977423292097?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/WJSmNWjQP8c" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/4354102977423292097/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=4354102977423292097" title="7 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/4354102977423292097?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/4354102977423292097?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/WJSmNWjQP8c/maven-throwing-out-bath-water-keeping.html" title="Maven: Throwing out the bath water, keeping the baby" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">7</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2009/10/maven-throwing-out-bath-water-keeping.html</feedburner:origLink></entry><entry gd:etag="W/&quot;CUcFR3Y8cCp7ImA9WxNVGE4.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-3752358355105410058</id><published>2009-10-29T09:11:00.001-07:00</published><updated>2009-10-29T09:16:56.878-07:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-10-29T09:16:56.878-07:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="cappuccino" /><title>Brew up a Rich Web Application with Cappuccino</title><content type="html">Last night I gave a fun talk on the Cappuccino framework to a handful of Portland JavaScript Admirers.&lt;div style="width:425px;text-align:left" id="__ss_2376418"&gt;&lt;a style="font:14px Helvetica,Arial,Sans-serif;display:block;margin:12px 0 3px 0;text-decoration:underline;" href="http://www.slideshare.net/hlship/brew-up-a-rich-web-application-with-cappuccino" title="Brew up a Rich Web Application with Cappuccino"&gt;Brew up a Rich Web Application with Cappuccino&lt;/a&gt;&lt;object style="margin:0px" width="425" height="355"&gt;&lt;param name="movie" value="http://static.slidesharecdn.com/swf/ssplayer2.swf?doc=cappuccino-key-091029110324-phpapp02&amp;stripped_title=brew-up-a-rich-web-application-with-cappuccino" /&gt;&lt;param name="allowFullScreen" value="true"/&gt;&lt;param name="allowScriptAccess" value="always"/&gt;&lt;embed src="http://static.slidesharecdn.com/swf/ssplayer2.swf?doc=cappuccino-key-091029110324-phpapp02&amp;stripped_title=brew-up-a-rich-web-application-with-cappuccino" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="425" height="355"&gt;&lt;/embed&gt;&lt;/object&gt;&lt;div style="font-size:11px;font-family:tahoma,arial;height:26px;padding-top:2px;"&gt;View more &lt;a style="text-decoration:underline;" href="http://www.slideshare.net/"&gt;documents&lt;/a&gt; from &lt;a style="text-decoration:underline;" href="http://www.slideshare.net/hlship"&gt;Howard Lewis Ship&lt;/a&gt;.&lt;/div&gt;&lt;/div&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-3752358355105410058?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/9mGTpnBYVZw" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/3752358355105410058/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=3752358355105410058" title="2 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/3752358355105410058?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/3752358355105410058?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/9mGTpnBYVZw/brew-up-rich-web-application-with.html" title="Brew up a Rich Web Application with Cappuccino" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">2</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2009/10/brew-up-rich-web-application-with.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DkAFQ3g5eSp7ImA9WxNXF08.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-8841056101923839314</id><published>2009-10-04T12:01:00.000-07:00</published><updated>2009-10-04T23:38:32.621-07:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-10-04T23:38:32.621-07:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="clojure" /><category scheme="http://www.blogger.com/atom/ns#" term="cascade" /><title>Cascade Exception Reporting</title><content type="html">I've been taking a little time from my billable projects to continue working on &lt;a href="http://github.com/hlship/cascade"&gt;Cascade&lt;/a&gt;. One feature that's very important to me is to have great exception reporting, akin to Tapestry's.  Here's a current snapshot of where I am:&lt;p&gt;
&lt;div class="separator" style="clear: both; text-align: center;"&gt;
&lt;a href="http://4.bp.blogspot.com/_OZa40fWwJDM/SsjvqlGLMhI/AAAAAAAAACY/6k_91-tYZ4w/s1600-h/hidden.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" src="http://4.bp.blogspot.com/_OZa40fWwJDM/SsjvqlGLMhI/AAAAAAAAACY/6k_91-tYZ4w/s400/hidden.png" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/div&gt;
&lt;p&gt;
This is very Tapestry-like (I've even borrowed the CSS styles). You can even see the start of the Request object's properties being displayed.&lt;p&gt;
Something to notice here: &lt;i&gt;Clojure stack frames are in Clojure syntax.&lt;/i&gt; To appreciate this, see what you get when you click the "Display hidden detail" button:&lt;p&gt;
&lt;div class="separator" style="clear: both; text-align: center;"&gt;
&lt;a href="http://4.bp.blogspot.com/_OZa40fWwJDM/SsjwDs9Lt7I/AAAAAAAAACg/VmHCNWOfl3w/s1600-h/expanded.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" src="http://4.bp.blogspot.com/_OZa40fWwJDM/SsjwDs9Lt7I/AAAAAAAAACg/VmHCNWOfl3w/s400/expanded.png" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/div&gt;
&lt;p&gt;
The exception report view is omitting a lot of clojure.lang internals, and it is working backwards from the mangled Java class name to the Clojure namespace and function name. This, plus only displaying the stack trace for the root exception, makes it much more reasonable to figure out where problems are actually occurring.&lt;p&gt;
I expect to expand this further, adding a pop-up or hover window to display Clojure source associated with the stack frame.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-8841056101923839314?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/G0Cr3kt9qGg" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/8841056101923839314/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=8841056101923839314" title="8 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/8841056101923839314?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/8841056101923839314?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/G0Cr3kt9qGg/cascade-exception-reporting.html" title="Cascade Exception Reporting" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://4.bp.blogspot.com/_OZa40fWwJDM/SsjvqlGLMhI/AAAAAAAAACY/6k_91-tYZ4w/s72-c/hidden.png" height="72" width="72" /><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">8</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2009/10/cascade-exception-reporting.html</feedburner:origLink></entry><entry gd:etag="W/&quot;C04MQn06fip7ImA9WxNXFUU.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-8012684243144826439</id><published>2009-10-03T07:25:00.000-07:00</published><updated>2009-10-03T07:59:43.316-07:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-10-03T07:59:43.316-07:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="tapestry" /><category scheme="http://www.blogger.com/atom/ns#" term="scriptaculous" /><category scheme="http://www.blogger.com/atom/ns#" term="prototype" /><category scheme="http://www.blogger.com/atom/ns#" term="customizing" /><title>Tapestry 5.1 and IE 8 -- Customizing Tapestry</title><content type="html">&lt;p&gt;Tapestry is nice enough to bundle the &lt;a href="http://prototypejs.org/"&gt;Prototype&lt;/a&gt; and &lt;a href="http://script.aculo.us/"&gt;Scriptaculous&lt;/a&gt; libraries its client-side support is wired against, which is very convenient ... until you find out the the packaged version is not compatible with your shiny new browser, such as Internet Explorer 8.

&lt;p&gt;
Tapestry IoC to the rescue: you can override where Tapestry looks for the Prototype &amp; Scriptaculous files (alas, it &lt;a href="https://issues.apache.org/jira/browse/TAP5-872"&gt;currently looks in the exact same place for them&lt;/a&gt;).  Where these files are stored, and how they are exposed to the client is controlled by two contributions inside TapestryModule:

&lt;pre&gt;
    public static void contributeFactoryDefaults(MappedConfiguration&amp;lt;String, String&amp;gt; configuration)
    {
        . . .

        configuration.add("tapestry.scriptaculous", "classpath:${tapestry.scriptaculous.path}");
        configuration.add("tapestry.scriptaculous.path", "org/apache/tapestry5/scriptaculous_1_8_2");

        . . .
    } 

    public static void contributeClasspathAssetAliasManager(MappedConfiguration&amp;lt;String, String&amp;gt; configuration,

      @Symbol(SymbolConstants.TAPESTRY_VERSION)
      String tapestryVersion,
      @Symbol("tapestry.scriptaculous.path")
      String scriptaculousPath)
    {
       . . .

       configuration.add("scriptaculous/" + tapestryVersion, scriptaculousPath);

       . . .
     }
&lt;/pre&gt;

&lt;p&gt;
The first contributions set where, on the classpath, the Prototype &amp; Scriptaculous files are located, defining symbols that can be referenced in various servers.  The second uses some of those symbols (and a few others) to map the classpath location to a URL (this is the job of the ClasspathAssetAliasManager service).

&lt;p&gt;
However, what's being contributed is Prototype &lt;strong&gt;1.6.0.3&lt;/strong&gt; and for compatibility with Internet Explorer 8, we need the latest and greatest: &lt;strong&gt;1.6.1&lt;/strong&gt;.  That's what 
&lt;a href="http://tapestry.formos.com/nightly/tapx/"&gt;tapx-prototype&lt;/a&gt; does.  And at its core it's just a couple of lines of code:

&lt;pre&gt;
public class PrototypeModule
{
    public void contributeFactoryDefaults(MappedConfiguration&amp;lt;String, String&amp;gt; configuration)
    {
        configuration.override("tapestry.scriptaculous.path", "com/howardlewisship/tapx/prototype");
    }

    public static void contributeClasspathAssetAliasManager(
            MappedConfiguration&lt;String, String&gt; configuration)
    {
        configuration.add("tapx-prototype/1.6.1", "com/howardlewisship/tapx/prototype");
    }
}
&lt;/pre&gt;

&lt;p&gt;
Notice that we can just override part of the configuration of one service (FactoryDefaults) and extend the configuration of another service (ClasspathAssetAliasManager) without disturbing anything else. This is Tapestry IoC in a nutshell!

&lt;p&gt;
&lt;a href="http://www.vanderburg.org/Blog"&gt;Glenn Vanderburg&lt;/a&gt; has been popularizing a terminology for extensible software: a "seam" is any point where existing software can be extended.  He's used to the Ruby world where, literally, every method is a seam.  Tapestry, too, is all seams ... every service, and (with more effort than Ruby) every method of every service is a seam, and lots of effort has gone into the design of Tapestry IoC to ensure that it can be extended &lt;em&gt;without&lt;/em&gt; massive cut-and-paste or other disruptions.

&lt;p&gt;
I'll be releasing the tapx code soon as a stable release, once I do a little more testing with IE8.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-8012684243144826439?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/k8Upq3qLJZI" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/8012684243144826439/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=8012684243144826439" title="2 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/8012684243144826439?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/8012684243144826439?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/k8Upq3qLJZI/tapestry-51-and-ie-8-customizing.html" title="Tapestry 5.1 and IE 8 -- Customizing Tapestry" /><author><name>Howard</name><uri>http://www.blogger.com/profile/04486596490758986709</uri><email>noreply@blogger.com</email><gd:extendedProperty name="OpenSocialUserId" value="09517791948272924282" /></author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">2</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2009/10/tapestry-51-and-ie-8-customizing.html</feedburner:origLink></entry></feed>
