<?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;C0ANSX47eyp7ImA9WxNbGUs.&quot;"><id>tag:blogger.com,1999:blog-4110180</id><updated>2009-11-22T23:36:38.003-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>656</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><link rel="self" href="http://feeds.feedburner.com/TapestryCentral" type="application/atom+xml" /><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="hub" href="http://pubsubhubbub.appspot.com" /><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="23 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">23</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="1 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">1</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><entry gd:etag="W/&quot;CEQERX8yfip7ImA9WxNQGU8.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-4840546860867903925</id><published>2009-09-25T16:45:00.000-07:00</published><updated>2009-09-25T16:45:04.196-07:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-09-25T16:45:04.196-07:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="off topic" /><title>Off topic: Quizissippi</title><content type="html">&lt;p&gt;For those of you in the Portland, OR area ... mark your calendars for Wednesday evenings at the &lt;a href="http://www.mississippipizza.com/"&gt;Mississippi Pizza Pub&lt;/a&gt; for Quizissippi, hosted by the wonderful &lt;a href="http://redmolly.typepad.com/"&gt;Red Molly&lt;/a&gt;.  It's a trivia contest with cash prizes ... and since it's run by Molly it will be very fun and very competitive and not quite like any trivia contest you've previously experienced.

&lt;p&gt;
The first Quizissippi is at &lt;strong&gt;7pm, on October 7th&lt;/strong&gt; in the &lt;a href="http://www.mississippipizza.com/atlantis.html"&gt;Atlantis Lounge&lt;/a&gt;. I'm already looking forward to Mark pouring some of his great drinks, and Molly dazzling us all with her cleverness.  And maybe some quick cash ... if I'm smart enough!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-4840546860867903925?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/U6Kc4mWzZyI" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/4840546860867903925/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=4840546860867903925" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/4840546860867903925?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/4840546860867903925?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/U6Kc4mWzZyI/off-topic-quizissippi.html" title="Off topic: Quizissippi" /><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/09/off-topic-quizissippi.html</feedburner:origLink></entry><entry gd:etag="W/&quot;A0EHRn4zeCp7ImA9WxNQGUU.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-5444510435215898969</id><published>2009-09-25T14:02:00.000-07:00</published><updated>2009-09-26T11:27:17.080-07:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-09-26T11:27:17.080-07:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="tapestry" /><category scheme="http://www.blogger.com/atom/ns#" term="firefox" /><category scheme="http://www.blogger.com/atom/ns#" term="ie" /><title>Joys of cross-browser development</title><content type="html">&lt;p&gt;
I'm working on a dependent drop-down list inside my Tapestry application (one I'm building for a client).  Guess what?  They fire the critical "change" even completely differently.

&lt;p&gt;
In FF, change is fired when:
&lt;ul&gt;
&lt;li&gt;You hit enter after tabbing into the select and changing its value
&lt;li&gt;You tab out of the field after changing its value with the up and down arrows
&lt;li&gt;You mouse click on the select, then click on an entry in the pop up list
&lt;/ul&gt;

&lt;p&gt;
This is sensible, even if it does not, perhaps, &lt;a href="http://www.nofluffjuststuff.com/blog/scott_leberknight/2005/04/onclick_versus_onchange_in_ie_and_firefox"&gt;match the spec&lt;/a&gt;.

&lt;p&gt;
In IE 6:
&lt;ul&gt;
&lt;li&gt;After using the mouse to select a value from the popup
&lt;li&gt;After changing the value with the up and down arrows
&lt;/ul&gt;

&lt;p&gt;In other words, any visual change to the select causes an immediate change event.
Of course, I need to work around IE's behavior since it will result in way too many Ajax requests.  My event handler needs to set a timer and wait some time, perhaps 1/4 seconds, before doing the Ajax update (but only if another change event hasn't been triggered).  Fortunately, I have &lt;a href="http://api.prototypejs.org/language/function.html#delay-instance_method"&gt;Prototype's delay&lt;/a&gt; on my side.

&lt;p&gt;
Meanwhile, who implements the spec correctly? Here's what it says:

&lt;blockquote&gt;
The onchange event occurs when a control loses the input focus and its value has been modified since gaining focus. This attribute applies to the following elements: INPUT, SELECT, and TEXTAREA
&lt;/blockquote&gt;

&lt;p&gt;
So, when using mouse selection, FF fires early (before the tab out of the field, or when you hit enter), and IE fires early all the time.

&lt;p&gt;And for those who have asked ... yes, I'll "productize" this code and put it into Tapestry 5.2!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-5444510435215898969?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/tuiO4fnwPZ4" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/5444510435215898969/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=5444510435215898969" title="1 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/5444510435215898969?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/5444510435215898969?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/tuiO4fnwPZ4/joys-of-cross-browser-development.html" title="Joys of cross-browser development" /><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/09/joys-of-cross-browser-development.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DEQNRnsyeip7ImA9WxNQGEw.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-1136887006937677609</id><published>2009-09-24T11:19:00.000-07:00</published><updated>2009-09-24T11:19:57.592-07:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-09-24T11:19:57.592-07:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="tapestry" /><category scheme="http://www.blogger.com/atom/ns#" term="devoxx" /><category scheme="http://www.blogger.com/atom/ns#" term="clojure" /><title>Devoxx 2009: Tapestry and Clojure</title><content type="html">&lt;p&gt;&lt;img src="http://www.devoxx.com/s/1116/1/DV09/_/download/userResources/DV09/logo" alt="devoxx logo" style="float: right;"/&gt;
Coming up in November ... I'll be at the &lt;a href="http://www.devoxx.com/display/DV09/Home"&gt;Devoxx 2009&lt;/a&gt; conference in
Antwerp, Belgium.  I'll be presenting two 60 minute talks:  one as an introduction to
&lt;a href="http://www.devoxx.com/display/DV09/Tapestry+5"&gt;Tapestry (Nov 20th, 10:30-11:30, Room 9)&lt;/a&gt;
and another as an introduction to
&lt;a href="http://www.devoxx.com/display/DV09/Clojure"&gt;Clojure (Nov 18th, 
12:00-13:00, Room 6)&lt;/a&gt;.  Given that my existing talks take closer to 90 minutes, this is going to take fast talking and a bit of editing!

&lt;p&gt;
Devoxx is a very cool conference, run by the user group community and very well organized. It's one of the best places to speak because of the quality of the A/V:  it takes place in a movie theater, with giant screens and plush, stadium seating.

&lt;p&gt;
This is my first return to Devoxx since 2004, when it was  still called Javopolis. With luck, &lt;em&gt;this time&lt;/em&gt;, I won't succumb to jet lag and comfy seats and start snoring in someone's session!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-1136887006937677609?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/8F1VvRKusNQ" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/1136887006937677609/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=1136887006937677609" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/1136887006937677609?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/1136887006937677609?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/8F1VvRKusNQ/devoxx-2009-tapestry-and-clojure.html" title="Devoxx 2009: Tapestry and 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/2009/09/devoxx-2009-tapestry-and-clojure.html</feedburner:origLink></entry><entry gd:etag="W/&quot;A04MQX09eSp7ImA9WxNQFk4.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-3213469920340373151</id><published>2009-09-22T10:18:00.000-07:00</published><updated>2009-09-22T10:19:40.361-07:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-09-22T10:19:40.361-07:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="cappuccino" /><category scheme="http://www.blogger.com/atom/ns#" term="nfjs" /><category scheme="http://www.blogger.com/atom/ns#" term="clojure" /><category scheme="http://www.blogger.com/atom/ns#" term="dsl" /><title>No Fluff Just Stuff Seattle Roundup</title><content type="html">&lt;p&gt;It was a huge pleasure to be speaking at a No Fluff Just Stuff conference again, it's been much too long. This was a good show with a lot of folks coming down from Vancouver, BC to attend.

&lt;p&gt;
This was an odd show for me, as we didn't do any Tapestry talks at all. However, all three of my talks (an introduction to &lt;a href="http://cappuccino.org"&gt;Cappuccino&lt;/a&gt;, and two talks on &lt;a href="http://clojure.org"&gt;Clojure&lt;/a&gt;) were well attended, with good questions and some fun discussions between sessions. I'm looking forward to doing these same sessions, with a number of improvements, in the Spring when NFJS starts back up.

&lt;p&gt;
Interestingly, I was busy writing Clojure code almost continuously in the back row of other speaker's sessions ... but this didn't stop me from appreciating &lt;a href="http://thirstyhead.com/"&gt;Scott Davis&lt;/a&gt;'s great talks on &lt;a href="http://grails.org/"&gt;Grails&lt;/a&gt;, and on "deconstructing Web 2.0".  Also, I had a lot of interaction with Brian Goetz ("are all web application's broken"). As usual, as soon as you mix Java with concurrency, it's a bit sobering. Tapestry helps here, but is not a complete panacea against some of the "nightmare scenarios" that may become more commonplace as the number of CPU cores increases.

&lt;p&gt;
I'm getting more adept at advanced Clojure; I've been busy adapting the concept of 
&lt;a href="http://clojure.org/special_forms#let"&gt;parameter destructuring&lt;/a&gt; to pulling data out of the request path and query parameters.

&lt;pre&gt;
(defaction increment-count
  {:path "count/increment"}
  [env] 
  [count :int] ; destructure the value after "/increment" as an integer
  (send-redirect env (link env show-counter [(inc count)])))
&lt;/pre&gt;

&lt;p&gt;
This defines a Cascade action, mapped to the "count/increment" path. A number is expected to follow (i.e., &lt;code&gt;/count/increment/4&lt;/code&gt;) and the job of this action is to increment that value and redirect the user to the show-counter view.

&lt;p&gt;
Cascade actions are still functions, thus the normal parameter declaration: &lt;code&gt;[env]&lt;/code&gt;. Cascade views and actions always take a single parameter, a map called "the environment" that provides access to Cascade-specific values as well as Servlet API objects.

&lt;p&gt;The path destructuring follows: &lt;code&gt;[count :int]&lt;/code&gt; which destructures the first "extra path" value as an integer. There's a number of built-in parsers referenced by keywords such as :int, or you can supply a function that takes a string and returns a value.  If another pair of values is given, that applies to the next value in the path.

&lt;p&gt;I'm finding this a pretty elegant way to express this secondary aspect of mapping URLs to view and actions; there's additional syntax for destructuring query parameters instead of path values. I may extend this further, to handle default values for missing terms in the path.

&lt;p&gt;
I try to capture some of this in my Clojure talks; the idea of growing the language up, adding the new features you need. From one perspective, this is just a domain specific language, but in the Lisp world, that concept is so deeply entrenched that there isn't even a word for it beyond "programming".  This is one part of the overall Clojure picture that gets me very excited, and I'm continuing to struggle with how to express this to people for whom even the Lisp syntax is alien and off-putting.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-3213469920340373151?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/a2ofbXVmyrs" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/3213469920340373151/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=3213469920340373151" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/3213469920340373151?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/3213469920340373151?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/a2ofbXVmyrs/no-fluff-just-stuff-seattle-roundup.html" title="No Fluff Just Stuff Seattle Roundup" /><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/09/no-fluff-just-stuff-seattle-roundup.html</feedburner:origLink></entry><entry gd:etag="W/&quot;A08MQ3wzeyp7ImA9WxNQFUg.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-8666259835740438222</id><published>2009-09-21T12:04:00.000-07:00</published><updated>2009-09-21T12:04:42.283-07:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-09-21T12:04:42.283-07:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="tapestry" /><category scheme="http://www.blogger.com/atom/ns#" term="apachecon" /><title>Speaking at ApacheCon 2009</title><content type="html">&lt;div class="separator" style="clear: both; text-align: center;"&gt;
&lt;a href="http://us.apachecon.com/c/acus2009/sessions/277"&gt;&lt;img border="0" height="26" src="http://us.apachecon.com/page_attachments/0000/0223/468x60_speaker.gif" width="200" /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;
Just a reminder ... I'll be spreading the good word about Tapestry at ApacheCon this year. My session is &lt;a href="http://us.apachecon.com/c/acus2009/sessions/277"&gt;Tapestry 5: Java Power, Scripting Ease!&lt;/a&gt; on Wed Nov 04 at 11am.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-8666259835740438222?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/ggg3WTPuGLc" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/8666259835740438222/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=8666259835740438222" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/8666259835740438222?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/8666259835740438222?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/ggg3WTPuGLc/speaking-at-apachecon-2009.html" title="Speaking at 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">0</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2009/09/speaking-at-apachecon-2009.html</feedburner:origLink></entry><entry gd:etag="W/&quot;C0ECQX8ycCp7ImA9WxNQEU4.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-2263151812969598245</id><published>2009-09-16T12:59:00.000-07:00</published><updated>2009-09-16T13:07:40.198-07:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-09-16T13:07:40.198-07:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="tapestry" /><category scheme="http://www.blogger.com/atom/ns#" term="performance" /><category scheme="http://www.blogger.com/atom/ns#" term="wicket" /><category scheme="http://www.blogger.com/atom/ns#" term="seam" /><category scheme="http://www.blogger.com/atom/ns#" term="jsf" /><title>Tapestry Performance Benchmark (vs. Wicket, JSF, etc.)</title><content type="html">&lt;p&gt;Peter Thomas has created a detailed &lt;a href="http://ptrthomas.wordpress.com/2009/09/14/perfbench-update-tapestry-5-and-grails/"&gt;performance analysis of Wicket, Tapestry, Grails and Seam&lt;/a&gt;. It's an interesting read from non-Tapestry user's perspective, and complements &lt;a href="http://blog.gidley.co.uk/2009/05/tapestry-load-testing-round-up.html"&gt;Ben Gidley's findings&lt;/a&gt;.

&lt;p&gt;
He's measuring raw performance and Wicket narrowly bests Tapestry in most categories, with Seam pretty close and Grails much further out.

&lt;p&gt;
I'm disturbed by some of his problems developing the application (with respect to adding client-side credit card number validation) and there's no mention of Tapestry's other qualities, such as live class reloading, exception reporting, etc. Still, criticism of Tapestry's documentation hits close to home (and it, alas, too fair). Accurate and very complete, but not organized for a beginner ... something I'm hoping to address over the next few months.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-2263151812969598245?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/4CUSOHpw2VQ" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/2263151812969598245/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=2263151812969598245" title="4 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/2263151812969598245?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/2263151812969598245?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/4CUSOHpw2VQ/tapestry-performance-benchmark-vs.html" title="Tapestry Performance Benchmark (vs. Wicket, JSF, etc.)" /><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/09/tapestry-performance-benchmark-vs.html</feedburner:origLink></entry><entry gd:etag="W/&quot;A0QFR3c8fCp7ImA9WxNQEk0.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-6353986100893822294</id><published>2009-09-16T12:56:00.001-07:00</published><updated>2009-09-17T10:41:56.974-07:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-09-17T10:41:56.974-07:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="tapestry" /><category scheme="http://www.blogger.com/atom/ns#" term="books" /><title>Like Tapestry? Read German? There's a book for you!</title><content type="html">&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://www.addison-wesley.de/media_local/Shop_U1Thumbs/9783827328441.jpg"&gt;&lt;img style="float:right; margin:0 0 10px 10px;cursor:pointer; cursor:hand;width: 120px; height: 172px;" src="http://www.addison-wesley.de/media_local/Shop_U1Thumbs/9783827328441.jpg" border="0" alt="book cover" /&gt;&lt;/a&gt;
&lt;p&gt;Igor Drobiazko has just announced his new book, 
&lt;a href="http://www.addison-wesley.de/main/main.asp?page=home/bookdetails&amp;ProductID=174975"&gt; Die Entwicklung von Webanwendungen mit Leichtigkeit!
 &lt;/a&gt; (The development of web applications with ease!). 

&lt;p&gt;Alas, it is in German, though he hopes good sales in Germany may lead to a translation for the English speaking market.  It's available as an e-book now, and in hardcover at the end of September.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-6353986100893822294?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/uDyTyq7w86E" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/6353986100893822294/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=6353986100893822294" title="2 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/6353986100893822294?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/6353986100893822294?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/uDyTyq7w86E/like-tapestry-read-german-theres-book.html" title="Like Tapestry? Read German? There's a book for you!" /><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/09/like-tapestry-read-german-theres-book.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DEYMSHw-cCp7ImA9WxNRFUw.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-8063630285767025031</id><published>2009-09-09T10:06:00.000-07:00</published><updated>2009-09-09T10:09:49.258-07:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-09-09T10:09:49.258-07:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="tapestry" /><category scheme="http://www.blogger.com/atom/ns#" term="streambank" /><category scheme="http://www.blogger.com/atom/ns#" term="formos" /><title>Formos in the news</title><content type="html">&lt;p&gt;
I may no longer work at Formos, but I'm still pleased to see a nice writeup about
&lt;a href="http://columbian.com/article/20090906/BIZ01/709069998/"&gt;Formos and StreamBank&lt;/a&gt;. StreamBank is an application that helps land owners and The Freshwater Trust work to improve stream health and water quality across Oregon. The article doesn't state this, but StreamBank is written in Tapestry (it started as Tapestry 4 and was ported to Tapestry 5).&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-8063630285767025031?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/kKpvDtsccbY" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/8063630285767025031/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=8063630285767025031" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/8063630285767025031?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/8063630285767025031?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/kKpvDtsccbY/formos-in-news.html" title="Formos in the news" /><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/09/formos-in-news.html</feedburner:origLink></entry><entry gd:etag="W/&quot;C0AMRHszcSp7ImA9WxNRF04.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-4341465422372672124</id><published>2009-08-28T12:46:00.000-07:00</published><updated>2009-09-11T22:03:05.589-07:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-09-11T22:03:05.589-07:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="clojure" /><title>Keeping track of Clojure dependencies</title><content type="html">&lt;p&gt;I've been coding more of &lt;a href="http://github.com/hlship/cascade"&gt;Cascade&lt;/a&gt; and I'm tending to do lots of small namespaces.  I'm concerned about circular dependencies, so I've been using OmniGraffle to keep track of what uses what:

&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/_OZa40fWwJDM/Spg3a8DjELI/AAAAAAAAACQ/b_qBVLxA8Qo/s1600-h/cascade-packages-20090828.png"&gt;&lt;img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 320px; height: 120px;" src="http://1.bp.blogspot.com/_OZa40fWwJDM/Spg3a8DjELI/AAAAAAAAACQ/b_qBVLxA8Qo/s320/cascade-packages-20090828.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5375107091203821746" /&gt;&lt;/a&gt;

&lt;p&gt;
I end up keeping this open in the background and updating it manually as I add new namespaces or change dependencies. I'm &lt;em&gt;pretty sure&lt;/em&gt; it's accurate.

&lt;p&gt;It does raise the question ... am I using the correct level of granularity? I think I am, and the individual files are pretty short:

&lt;pre&gt;
$ find src/main/clojure -name \*.clj | xargs wc -l
      51 src/main/clojure/cascade/config.clj
     115 src/main/clojure/cascade/dispatcher.clj
      92 src/main/clojure/cascade/dom.clj
     107 src/main/clojure/cascade/filter.clj
      51 src/main/clojure/cascade/internal/parse_functions.clj
      94 src/main/clojure/cascade/internal/parser.clj
     205 src/main/clojure/cascade/internal/utils.clj
     113 src/main/clojure/cascade/internal/viewbuilder.clj
      36 src/main/clojure/cascade/jetty.clj
      39 src/main/clojure/cascade/logging.clj
      29 src/main/clojure/cascade/map_utils.clj
      56 src/main/clojure/cascade/mock.clj
      73 src/main/clojure/cascade/path_map.clj
      41 src/main/clojure/cascade/pipeline.clj
      30 src/main/clojure/cascade/urls.clj
      81 src/main/clojure/cascade.clj
    1213 total
&lt;/pre&gt;


&lt;p&gt; ... and that includes the ASL header comment in each file.  Short and sweet.
&lt;p&gt;
Maintaining this chart seems like something I should automate at some point, however: It should be possible to use the reader to parse a Clojure source file without evaluating it; the first form should be the (ns), from which can be expanded the used and required namespaces (with a bit of effort, because of Clojure's very concise syntax). One more project for the back burner ...&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-4341465422372672124?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/N1gyznGNupM" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/4341465422372672124/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=4341465422372672124" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/4341465422372672124?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/4341465422372672124?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/N1gyznGNupM/keeping-track-of-clojure-dependencies.html" title="Keeping track of Clojure dependencies" /><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://1.bp.blogspot.com/_OZa40fWwJDM/Spg3a8DjELI/AAAAAAAAACQ/b_qBVLxA8Qo/s72-c/cascade-packages-20090828.png" height="72" width="72" /><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2009/08/keeping-track-of-clojure-dependencies.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DEcERn07fyp7ImA9WxNSE0Q.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-1437769225774171269</id><published>2009-08-27T10:56:00.000-07:00</published><updated>2009-08-27T11:00:07.307-07:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-08-27T11:00:07.307-07:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="consulting" /><title>Return to Independent Consulting</title><content type="html">&lt;p&gt;
I'd like to announce to the Tapestry community that I've returned to
independent consulting. As an independent consultant, I'll have more opportunities to pursue training, mentoring, and project work that did not fit
with Formos' overall goals.

&lt;p&gt;
Formos continues to be committed to Tapestry, and to maintaining the Tapestry360 web site. I'd like to thank Matt Tunnel, President of Formos, for
the opportunities he's provided: a "dream job" that let me focus on completing Tapestry 5.0 and 5.1, with a scope of features far beyond what I had originally envisioned when I started Tapestry 5 over four years ago.

&lt;p&gt;
Now is a new chapter; I'm starting to search for my next dream job, while
actively seeking out new Tapestry training, mentoring and support projects,
as well as working with my existing clients. In addition, I'm using my 
improved freedom to pursue other important technologies beyond Tapestry, such
as Clojure, Cappuccino, and CouchDB. I expect to be able to offer the same
kind of compelling training and project work in these technologies as I
currently provide for Tapestry.

&lt;p&gt;
I'm also taking this time to pursue one of the opportunities I could not take
on while at Formos: a Tapestry 5 book. I'm currently contacting a number of
different publishers to find the best home for a new book specifically
about Tapestry 5.  

&lt;p&gt;
I'd also like to thank the Tapestry community for all the enthusiasm and
dedication that you've given to Tapestry. I'm looking forward to helping you
create even more insanely great applications!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-1437769225774171269?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/KVrHpMevLjE" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/1437769225774171269/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=1437769225774171269" title="7 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/1437769225774171269?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/1437769225774171269?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/KVrHpMevLjE/return-to-independent-consulting.html" title="Return to Independent Consulting" /><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/08/return-to-independent-consulting.html</feedburner:origLink></entry><entry gd:etag="W/&quot;CUACSXY9eyp7ImA9WxNTFEw.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-5457684530698159681</id><published>2009-08-16T01:57:00.000-07:00</published><updated>2009-08-16T02:09:28.863-07:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-08-16T02:09:28.863-07:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="java" /><category scheme="http://www.blogger.com/atom/ns#" term="meta-programming" /><title>Article: Meta-Programming Java</title><content type="html">&lt;p&gt;
In the last couple of years, if you mention the term
&lt;strong&gt;meta-programming&lt;/strong&gt;, people's ears perk up ... and they
start looking around for Ruby.  That's fair; Ruby makes a lot of
meta-programming concepts very, very easy. However, that doesn't mean
you can't do &lt;em&gt;any&lt;/em&gt; meta-programming in Java; you just are a
bit more limited and need a lot more infrastructure.

&lt;p&gt;
Tapestry 5, both the web framework and the underlying Inversion of
Control container, is rife with meta-programming options. Let's talk
about one of the most versatile: the &lt;a href="http://en.wikipedia.org/wiki/Thunk"&gt;thunk&lt;/a&gt;.

&lt;h2&gt;Thunks and Laziness&lt;/h2&gt;

&lt;p&gt;
A thunk is a placeholder for a value to be computed as-needed. The
&lt;a href="http://haskell.org/"&gt;Haskell&lt;/a&gt; programming language makes great use of these; thunks are the
essense of lazy programming: each thunk represents a set of parameters
to a function&lt;sup&gt;&lt;a href="#thunkfn1"&gt;1&lt;/a&gt;&lt;/sup&gt; and the function itself.

&lt;p&gt;
The upshot of this is that when you see a function call (or
other expression) in
Haskell code, what really happens is that a thunk of the invocation of that function
is created to capture the values to be passed in (some of
which may themselves be thunks of other expressions). Its only when
the value is needed, when the result of the expression is used in some
other expression that is evaluated, that the thunk itself gets
evaluated; the function is invoked, the return value is cached in the
thunk and returned. This makes the order in which things happen in
Haskell very difficult to predict, especially from the outside. Because of thunks,
algorithms that look tail recursive aren't (the recursive call is just another thunk, evaulated
serially). Further, algorithms that appear to be infinite, aren't: the thunks ensure
that just values that are actually needed are ever computed.

&lt;p&gt;It's an
elegant and powerful approach, and it's even fast, because the fastest code is the code that is
never executed in the first place.

&lt;p&gt;
Other languages have this feature; &lt;a
href="http://clojure.org"&gt;Clojure&lt;/a&gt; reflects its Lisp heritage in
that almost everything operates in terms of accessing, iterating and
transforming collections ... and all of those collection operations
are lazy as well. Unlike Haskell, this is more a function of a
carefully crafted standard library than a direct offshoot of the
language, but the end result is quite similar.


&lt;p&gt;
But what happens when you want to accomplish some of these features
(such as lazy evaluation) within the tight constraints of standard Java?
That's when you need to get creative!


&lt;h2&gt;Thunks in Tapestry 5&lt;/h2&gt;

&lt;p&gt;
Tapestry 5 uses thunks in many different places; the most common one
is the use of proxies for Tapestry 5 IoC services.  In Tapestry 5
every service has an interface&lt;sup&gt;&lt;a href="#thunkfn2"&gt;2&lt;/a&gt;&lt;/sup&gt;.
Let's take a peek at a typical service in Tapestry 5, to illustrate the
typed-thunk concept. 

&lt;h4&gt;Listing 1: ComponentMessagesSource.java&lt;/h4&gt;
&lt;pre&gt;public interface ComponentMessagesSource
{
    Messages getMessages(ComponentModel componentModel, Locale locale);

    InvalidationEventHub getInvalidationEventHub();
}
&lt;/pre&gt;

&lt;p&gt;
The purpose of the ComponentMessagesSource service is to provide a Messages object representing a particular component's message catalog. This is part of Tapestry's localization support: every page and component has easy access to its own message bundle, which includes messages inherited from base components and from a global message catalog.


&lt;p&gt;
A central tenet of Tapestry 5 is that service instantiation is lazy: services are only constructed as needed. What does "as needed" mean?  It means, the first time any method of the service is invoked. This kind of lazy instantiation is accomplished by using thunks. So for a service such as ComponentMessagesSource, there will be a class somewhat like ComponentMessagesSourceThunk to handle the lazy instantiation:


&lt;h4&gt;Listing 2: ComponentMessagesSourceThunk.java&lt;/h4&gt;
&lt;pre&gt;public interface ComponentMessagesSourceThunk implements ComponentMessagesSource
{
    private final &lt;a
href="http://tapestry.apache.org/tapestry5.1/apidocs/org/apache/tapestry5/ioc/ObjectCreator.html"&gt;ObjectCreator&lt;/a&gt; creator;

    public ComponentMessagesSourceThunk(ObjectCreator creator) { this.creator = creator; }

    private ComponentMessagesSourceThunk delegate() { return (ComponentMessagesSourceThunk) creator.createObject(); }

    public Messages getMessages(ComponentModel componentModel, Locale locale)
    {
        return delegate().getMessages(componentModel, locale);
    }

    public InvalidationEventHub getInvalidationEventHub()
    {
        return delegate().getInvalidationEventHub();
    }
}
&lt;/pre&gt;

&lt;p&gt;You won't find the above class in the Tapestry source code: it is generated on-the-fly by Tapestry. That's great, because I know I'd hate to have to supply a service interface, a service implementation &lt;em&gt;and&lt;/em&gt; a thunk class for each service; the interface and implementation is already plenty! One of the reasons that Tapestry all but requires
that services have a service interface is to support the automatic creation of thunks or other proxies around the interface. 

&lt;p&gt;However, you can see the pattern: every method of the interface is, of course, implemented in the thunk. That's what it means to implement an interface. Each method obtains
the &lt;em&gt;delegate&lt;/em&gt; and then re-invokes the &lt;em&gt;same&lt;/em&gt; method with the &lt;em&gt;same&lt;/em&gt; parameters on the delegate. The trick is that the first time any of these methods are invoked, the delegate does not yet exist.  The ObjectCreator will create the delegate object during that first invocation, and keep returning it subsequently. That's the essence of lazy instantiation.

&lt;p&gt;
The point here is that for any interface, you can create a typed-thunk that can stand in for the real object, hiding the real object's lifecycle: it gets created on demand by the ObjectCreator. Code that uses the thunk has no way of telling the thunk from the real objects ... the thunk implements all the methods of the interface and performs the right behaviors when those methods get invoked.

&lt;h2&gt;Creating Thunks Dynamically&lt;/h2&gt;

&lt;p&gt;Before we can talk about &lt;em&gt;using&lt;/em&gt; thunks, we need to figure out how to create them dynamically, at runtime. Let's start by specifying the interface for a service that can provide thunks on demand, then figure out the implementation of that service. 

&lt;h4&gt;Listing 3: ThunkCreator.java&lt;/h4&gt;
&lt;pre&gt;public interface ThunkCreator
{
    /**
     * Creates a Thunk of the given proxy type.
     *
     * @param proxyType     type of object to create (must be an interface)
     * @param objectCreator provides an instance of the same type on demand (may be invoked multiple times)
     * @param description   to be returned from the thunk's toString() method
     * @param &amp;lt;T&amp;gt;           type of thunk
     * @return thunk of given type
     */
    &amp;lt;T&amp;gt; T createThunk(Class&amp;lt;T&amp;gt; proxyType, ObjectCreator objectCreator, String description);
}
&lt;/pre&gt;

&lt;p&gt;Remember that this is just an automated way of producing instances of classes
similar to ComponentMessagesSourceThunk. A simple implementation of this service is possible
using JDK Proxies:

&lt;h4&gt;Listing 4: ThunkCreatorImpl.java&lt;/h4&gt;
&lt;pre&gt;public class ThunkCreatorImpl implements ThunkCreator
{
    public &amp;lt;T&amp;gt; T createThunk(Class&amp;lt;T&amp;gt; proxyType, final ObjectCreator objectCreator, final String description)
    {
        InvocationHandler handler = new InvocationHandler()
        {
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
            {
                if (method.getName().equals("toString") &amp;&amp; method.getParameterTypes().length == 0)
                    return description;

                return method.invoke(objectCreator.createObject(), args);
            }
        };

        Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                                              new Class[] { proxyType },
                                              handler);

        return proxyType.cast(proxy);
    }
}
&lt;/pre&gt;

&lt;p&gt;
JDK Proxies were introduced way back in JDK 1.3 and caused a real flurry of activity because they are so incredibly useful.  A call to Proxy.newProxyInstance() will create an object
conforming to the provided interfaces (here specified as the proxyType
parameter).  Every method invocation is routed through a single InvocationHandler object.  The
InvocationHandler simply re-routes method invocations to the object
returned from objectCreator.createObject().

&lt;p&gt;
Tapestry's implementation of
&lt;a href="http://tapestry.formos.com/nightly/tapestry5/apidocs/src-html/org/apache/tapestry5/ioc/internal/services/ThunkCreatorImpl.html"&gt;ThunkCreator&lt;/a&gt; uses the
&lt;a href="http://www.jboss.org/javassist/"&gt;Javassist&lt;/a&gt;
bytecode manipulation library to generate a
custom class at runtime. The generated class is much closer to the example CompnentMessagesSourceThunk; it doesn't use
JDK proxies or reflection. This means that Java's
Hotspot compiler can do a better job optimizing the code.  In reality, you'll be hard
pressed to spot a difference in performance unless you use these
thunks inside a very tight loop.

&lt;p&gt;
Great so far; now lets think about how we could use this in another
way.  What if you have a service that returns an object that is
expensive to construct and may not even get used?  An example of this in Tapestry is the
Messages object, obtained from the
ComponentMessagesSource
service. Building a Messages instance for a component
involves a lot of hunting around the classpath looking for properties
files, not just for the component but for its base-class and for
application-wide message bundles. That means a lot of I/O and and a lot of blocking, waiting for the disk drive to
catch up. In many cases, these Messages objects are injected
into components, but aren't used immediately. In terms of getting markup into the user's browser faster, avoiding
all of those file lookups and file reads until absolutely necessary is an appreciable win.


&lt;p&gt;
Our goal is to intercept the call to ComponentMessagesSource.getMessages() and capture the parameters to the method.
Instead of invoking the method, we want to return a thunk that encapsulates the method call.  This is where we can really
start to talk about &lt;strong&gt;meta-programming&lt;/strong&gt;, not just programming: we aren't going to change the ComponentMessagesSource service implementation to accomplish this, we are going to meta-program the service. This is a key point: &lt;em&gt;A Tapestry service is the sum of its interface, its implementation, and all the other parts provided by Tapestry&lt;/em&gt;. We can use Tapestry to augment the behavior of a service without changing the implementation of the service itself.

&lt;p&gt;
This approach is in stark contrast to, say, Ruby. When meta-programming Ruby you often end up writing and rewriting the methods defined by the class &lt;em&gt;in place&lt;/em&gt;. In Java, you will instead layer on new objects implementing the &lt;em&gt;same interface&lt;/em&gt; to provide the added behavior.

&lt;p&gt;Accomplishing all this is suprisingly easy ... given the infrastructure that Tapestry 5 IoC already provides.


&lt;h2&gt;Lazy Advice&lt;/h2&gt;

&lt;p&gt;
The goal with lazy advice is that invoking a method on a service short-circuits the method invocation: a thunk is returned that is
a replacement for the return value of the method.  Invoking a method on a thunk will invoke the actual service method, then re-invoke the method on the actual value returned from the method.

&lt;h4&gt;Image 1: Lazy Advice Thunk/&lt;/h4&gt;
&lt;img src="http://howardlewisship.com/images/thunk-calling.png" title="Lazy Advice Thunk"/&gt;

&lt;p&gt;
This is shown in image 1.  The service method is represented by the blue line. The advice intercepts the call (remembering the method parameters) and returns a thunk. Later, the caller invokes a method on the thunk (the green line). The thunk will
invoke the service method using the saved parameters (this is the lazy part), then re-invoke the method on the returned value.

&lt;p&gt;
To the caller, there is no evidence that the thunk even exists; the service method just returns faster than it should, and the
first method invocation on the return value takes a little longer than it should.

&lt;p&gt;
Now we know what the solution is going to look like .. but how do we make it actually happen? How do we get "in there" to
advise service methods?


&lt;h2&gt;Advising Service Methods&lt;/h2&gt;

&lt;p&gt;Tapestry's Inversion of Control Container is organized around modules: classes that define services. This is in contrast
to &lt;a href="http://www.springsource.org/"&gt;Spring&lt;/a&gt;, which relies on verbose XML files.  Tapestry uses a naming convention to figure out what methods of a module class do what.  Methods whose name starts with "build" define services (and are ultimately used to instantiate them).  Other method name prefixes have different meanings.

&lt;p&gt;Module method names prefixed with "advise" act as a hook for a limited amount of &lt;a href="http://en.wikipedia.org/wiki/Aspect-oriented_programming"&gt;Aspect Oriented Programming&lt;/a&gt;.  Tapestry
allows an easy way to provide &lt;em&gt;around advice&lt;/em&gt; on method invocations ... a more intrusive system such as
&lt;a href="http://www.eclipse.org/aspectj/"&gt;AspectJ&lt;/a&gt; can easily intercept access to fields or even the construction of classes and has more facilities for limiting the scope of advice so that it only applies to invocations in specific classes or packages. Of course, it works by significantly rewriting the bytecode of your classes and Tapestry's IoC container aims for a lighter touch.

&lt;p&gt;
Being able to advise service methods was originally intended to support logging of method entry and exit, or other cross-cutting converns such as managing transactions or enforcing security access constraints. However, the same mechanism can go much further, controlling &lt;em&gt;when&lt;/em&gt; method invocations occur, in much the same way that the lazy thunk described above operates.

&lt;p&gt;Listing 5 shows the method advice for the ComponentMessagesSource service.

&lt;h4&gt;Listing 5: TapestryModule.java&lt;/h4&gt;
&lt;pre&gt;    @Match("ComponentMessagesSource")
    public static void adviseLazy(LazyAdvisor advisor, MethodAdviceReceiver receiver)
    {
        advisor.addLazyMethodInvocationAdvice(receiver);
    }
&lt;/pre&gt;

&lt;p&gt;
This method is used to advise a specific service, identified by the service's unique id, here "ComponentMessagesSource".  An advisor method may advise many different services; we could use
glob names or regular expressions to match a wider range of services.  An advisor method recieves a
&lt;a href="http://tapestry.apache.org/tapestry5.1/apidocs/org/apache/tapestry5/ioc/MethodAdviceReceiver.html"&gt;MethodAdviceReceiver&lt;/a&gt; as a parameter; additional parameters are injected services. The intent of module classes is to contain a minimal amount of code,
so it makes sense to move the real work into a service, especially because it is so easy to inject services directly into
the advisor method.



&lt;p&gt;The LazyAdvisor service, built into Tapestry, does most of the work:

&lt;h4&gt;Listng 6: LazyAdvisorImpl.java&lt;/h4&gt;
&lt;pre&gt;public class LazyAdvisorImpl implements LazyAdvisor
{
    private final ThunkCreator thunkCreator;

    public LazyAdvisorImpl(ThunkCreator thunkCreator)
    {
        this.thunkCreator = thunkCreator;
    }

    public void addLazyMethodInvocationAdvice(MethodAdviceReceiver methodAdviceReceiver)
    {
        for (Method m : methodAdviceReceiver.getInterface().getMethods())
        {
            if (filter(m))
                addAdvice(m, methodAdviceReceiver);
        }
    }

    private void addAdvice(Method method, MethodAdviceReceiver receiver)
    {
        final Class thunkType = method.getReturnType();

        final String description = String.format("&amp;lt;%s Thunk for %s&amp;gt;",
                                                 thunkType.getName(),
                                                 InternalUtils.asString(method));

        MethodAdvice advice = new MethodAdvice()
        {
            /**
             * When the method is invoked, we don't immediately proceed. Intead, we return a thunk instance
             * that defers its behavior to the lazily invoked invocation.
             */
            public void advise(final Invocation invocation)
            {
                ObjectCreator deferred = new ObjectCreator()
                {
                    public Object createObject()
                    {
                        invocation.proceed();

                        return invocation.getResult();
                    }
                };

                ObjectCreator cachingObjectCreator = new CachingObjectCreator(deferred);

                Object thunk = thunkCreator.createThunk(thunkType, cachingObjectCreator, description);

                invocation.overrideResult(thunk);
            }
        };

        receiver.adviseMethod(method, advice);
    }

    private boolean filter(Method method)
    {
        if (method.getAnnotation(NotLazy.class) != null) return false;

        if (!method.getReturnType().isInterface()) return false;

        for (Class extype : method.getExceptionTypes())
        {
            if (!RuntimeException.class.isAssignableFrom(extype)) return false;
        }

        return true;
    }
}
&lt;/pre&gt;

&lt;p&gt;
The core of the LazyAdvisor service is in the addAdvice() method. A &lt;a href="http://tapestry.apache.org/tapestry5.1/apidocs/org/apache/tapestry5/ioc/MethodAdvice.html"&gt;MethodAdvice&lt;/a&gt; inner class is defined; the MethodAdvice interface has only a single method, advise().  The advise() method will be passed an &lt;a href="http://tapestry.apache.org/tapestry5.1/apidocs/org/apache/tapestry5/ioc/Invocation.html"&gt;Invocation&lt;/a&gt; that represents the method being invoked. The Invocation captures parameters passed in as well as the return value or any checked exceptions that are thrown.  Invoking the proceed() method continues on to the original method of the service&lt;sup&gt;&lt;a href="thunkfn3"&gt;3&lt;/a&gt;&lt;/sup&gt;.


&lt;p&gt;
At this point, the thunk encapsulates the original method invocation; we even have an object for that: the Invocation instance originally passed to the advise() method. Invoking any method on the thunk will cause the ObjectCreator.createObject() method to be triggered: this is where we finally invoke proceed() and return the value for the lazily invoked method.


  
&lt;h2&gt;Other uses for Thunks&lt;/h2&gt;

&lt;p&gt;
In essence, this thunk approach gives you the ability to control the context in which a method is executed: is it executed right now, or only when needed? It is only a little jump from that to executing the method in a background thread. In fact, Tapestry includes a &lt;a href="http://tapestry.formos.com/nightly/tapestry5/apidocs/org/apache/tapestry5/ioc/services/ParallelExecutor.html"&gt;ParellelExecutor&lt;/a&gt; service that can be used for just that.




&lt;h2&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;
Type-safe thunks are a powerful and flexible technique for controlling when (or even if) a method is invoked without
sacrificing type safety. Unlike more intrusive techniques that rely on manipulating the bytecode of existing classes, type-safe thunks can be easily and safely introduced into existing code bases. More than that, this exercise opens up many exciting possibilities: these techniques (coding to interfaces, multiple objects with the same interface, delegation) open up a path to a more fluid, more responsive, more elegant approach to coding complex behaviors and interactions ... while reducing the total line count and complexity of your code. 

&lt;p&gt;
One of the things I am most happy about in Tapestry is the way in which we can build up complex behaviors from simple pieces.  Everything stacks together, concisely and with minimum fuss:

&lt;ul&gt;
  &lt;li&gt;We can create a thunk around an ObjectCreator, to defer the instantiation of an object&lt;/li&gt;
  &lt;li&gt;We can capture a method invocation and convert that into an ObjectCreator and a lazy thunk&lt;/li&gt;
  &lt;li&gt;We can advise a method without changing the actual implementation, to provide the desired laziness&lt;/li&gt;
  &lt;li&gt;Tapestry can call an advisor method of our module when constructing the ComponentMessagesSource service&lt;/li&gt;
  &lt;li&gt;We can inject services that do the advising right into advisor methods&lt;/li&gt;
&lt;/ul&gt;



&lt;h4&gt;Footnotes&lt;/h4&gt;

&lt;p&gt;&lt;sup id="thunkfn1"&gt;1&lt;/sup&gt; Actually, all functions in Haskell take
exactly one parameter which is both mind-blowing and not
relevant to the discussion.

&lt;p&gt;&lt;sup id="thunkfn2"&gt;2&lt;/sup&gt; Services can be based on classes rather
than interfaces, but then you lose a lot of these interface-based
features, such as lazy proxies.

  &lt;p&gt;&lt;sup id="thunkfn3"&gt;3&lt;/sup&gt;Or, if the method has been advised multiple times, invoking proceed() may invoke the next
  piece of advice. For example, you may have added advice to a method for logging method entry and exit, and for managing database transactions as well as lazy evaluation.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-5457684530698159681?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/mzlntkeFZRg" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/5457684530698159681/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=5457684530698159681" title="2 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/5457684530698159681?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/5457684530698159681?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/mzlntkeFZRg/article-meta-programming-java.html" title="Article: Meta-Programming Java" /><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/08/article-meta-programming-java.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DEcMQn89fyp7ImA9WxNSE0Q.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-2366078591754098709</id><published>2009-08-15T02:26:00.000-07:00</published><updated>2009-08-27T11:01:23.167-07:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-08-27T11:01:23.167-07:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="tapestry" /><title>Detailed analysis of Tapestry 5</title><content type="html">&lt;p&gt;
&lt;a href="http://www.laliluna.de/tapestry-webframework-evaluation-test.html"&gt;Sebastian Hennebrueder&lt;/a&gt; has just finished a detailed analysis of Tapestry 5. He comes at it from a few odd angles (for instance, he likes PicoContainer and shows how to integrate it). After a few misteps, he reaches these conclusions:

&lt;blockquote&gt;
&lt;p&gt;
Once I overcame the first hurdles, I became more and more impressed. Building CRUD (create, read, update, delete) dialogs is incredible fast. The form component renders a form for a model, adding labels, input fields and validations. All this information is extracted from the model and its annotation and you don't have to write a single line of code. Here is the code for a complete form.

&lt;p&gt;&lt;code&gt;&amp;lt;t:beaneditform object="person"/&amp;gt;&lt;/code&gt;

&lt;p&gt;
You have control over the generated form and the possibility to change whatever you need either application wide or just in a single form. As a consequence, you get even less code than in a Ruby on Rails application. The learning curve is of course steeper than the one of the Stripes framework, but this is naturally. Stripes is a thin layer above the underlying technologies. Tapestry abstracts from the underlying technology in order to provide a lot of powerful functionality.

&lt;p&gt;
After having explored the functionality of the framework, writing my own components, writing mixins to extend existing components, I came to the conclusion that Tapestry is one of the most innovative frameworks and probably even the best candidate for enterprise applications.
&lt;/blockquote&gt;

&lt;p&gt;
To be honest, I think he makes the initial steps slighlty more complicated than they need to be and he properly criticizes the current state of the documentation. But he reaches the above conclusions, then goes into more detail, and finally outlines some performance data.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-2366078591754098709?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/nMuKIjMWBio" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/2366078591754098709/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=2366078591754098709" title="3 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/2366078591754098709?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/2366078591754098709?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/nMuKIjMWBio/detailed-analysis-of-tapestry-5.html" title="Detailed analysis of Tapestry 5" /><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">3</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2009/08/detailed-analysis-of-tapestry-5.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DUAHQX89cSp7ImA9WxNTE08.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-3417469399808733402</id><published>2009-08-15T02:09:00.000-07:00</published><updated>2009-08-15T02:15:30.169-07:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-08-15T02:15:30.169-07:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="clojure" /><category scheme="http://www.blogger.com/atom/ns#" term="oscon" /><title>Quickie video from OSCON</title><content type="html">&lt;object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="437" height="370" id="viddler"&gt;&lt;param name="movie" value="http://www.viddler.com/player/e03b3880/211.344/" /&gt;&lt;param name="allowScriptAccess" value="always" /&gt;&lt;param name="allowFullScreen" value="true" /&gt;&lt;embed src="http://www.viddler.com/player/e03b3880/211.344/" width="437" height="370" type="application/x-shockwave-flash" allowScriptAccess="always" allowFullScreen="true" name="viddler" &gt;&lt;/embed&gt;&lt;/object&gt;

&lt;p&gt;In this video (from &lt;a href="http://blog.envylabs.com/tag/oscon/"&gt;Greg Pollack&lt;/a&gt;),
I have a &lt;em&gt;very short&lt;/em&gt; segment discussing Clojure. Oddly, the fun part of what I said was clipped, which can be summarized as "Clojure is fast and I haven't had this much fun programming since I first learned Object Oriented programming (Objective-C, in 1995) or first started programming at age 13."  I'm surprised they cut it because a few people in the speaker's lounge actually clapped after I was done!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-3417469399808733402?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/-OJF3Nnlclw" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/3417469399808733402/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=3417469399808733402" title="0 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/3417469399808733402?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/3417469399808733402?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/-OJF3Nnlclw/quickie-video-from-oscon.html" title="Quickie video from OSCON" /><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/08/quickie-video-from-oscon.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DkcFSXY9cSp7ImA9WxJbGEg.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-5047517319433122914</id><published>2009-07-29T00:46:00.000-07:00</published><updated>2009-07-29T00:53:38.869-07:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-07-29T00:53:38.869-07:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="tapestry" /><category scheme="http://www.blogger.com/atom/ns#" term="cappuccino" /><category scheme="http://www.blogger.com/atom/ns#" term="nfjs" /><title>Tapestry 2009 Summer Tour: England, Estonia</title><content type="html">&lt;p&gt;
Just dropping a note from the road ... I'm currently in York, England for three days of Tapestry training here for a private client.  Next week, five days of training and mentoring in Tartu, Estonia.  I was a bit trepidatious about heading to Estonia, but now that I've read up on its history, I can't wait to get there.

&lt;p&gt;
After that, a bit of vacation in London and Paris ... then back to Portland (hopefully the temperatures will have dropped) to concentrate on Tapestry 5.1 documentation ... and learning Cappuccino for my upcoming talk at &lt;a href="http://www.nofluffjuststuff.com/conference/seattle/2009/09/home"&gt;The Pacific Northwest Software Symposium&lt;/a&gt;.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-5047517319433122914?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/S5fFRDhW6uQ" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/5047517319433122914/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=5047517319433122914" title="4 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/5047517319433122914?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/5047517319433122914?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/S5fFRDhW6uQ/tapestry-2009-summer-tour-england.html" title="Tapestry 2009 Summer Tour: England, Estonia" /><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/07/tapestry-2009-summer-tour-england.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DU8HRX45eyp7ImA9WxJUF0o.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-7612915750934730726</id><published>2009-07-16T09:38:00.000-07:00</published><updated>2009-07-16T13:57:14.023-07:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-07-16T13:57:14.023-07:00</app:edited><title>Infrequent commands</title><content type="html">&lt;p&gt;
An odd usability thought just hit me as I'm making some simple updates to my slide decks in Keynote.  Adding and positioning the page number is not something you can do without using the mouse (to click the button in the inspector and drag it into place). There's no menu item for this.

&lt;p&gt;That's normally OK with me ... it's a very infrequent operation so why take up valueable menu space (as well as valuable user comprehension space) with it?

&lt;p&gt;But the odd thing is that I think quite often, when you need to use an infrequent operation you need to use it a lot at once. That's probably why Microsoft applications have byzantine menu structures ... they never want anything to require the mouse and from a usability perspective they throw the baby out with the bath water over those infrequent commands.

&lt;p&gt;All I'm missing from Keynote is a simple macro-recording feature: turn on page numbers, select it, drag it to position, update its style.  I'd love to be able to run through my master slides (10 per presentation times 7 presentations!) and just it cmd-m or something.

&lt;p&gt;Of course, if Keynote had master-masters, I might be able to make the change in one place per presentation (which would apply to the ten masters and the 60 or 70 slides per presentation).&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-7612915750934730726?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/I6IWMSrLO70" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/7612915750934730726/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=7612915750934730726" title="3 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/7612915750934730726?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/7612915750934730726?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/I6IWMSrLO70/infrequent-commands.html" title="Infrequent commands" /><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">3</thr:total><feedburner:origLink>http://tapestryjava.blogspot.com/2009/07/infrequent-commands.html</feedburner:origLink></entry><entry gd:etag="W/&quot;DEEERHsycSp7ImA9WxJVFUs.&quot;"><id>tag:blogger.com,1999:blog-4110180.post-1178611148957672368</id><published>2009-07-02T13:10:00.000-07:00</published><updated>2009-07-02T13:30:05.599-07:00</updated><app:edited xmlns:app="http://www.w3.org/2007/app">2009-07-02T13:30:05.599-07:00</app:edited><category scheme="http://www.blogger.com/atom/ns#" term="intellij" /><category scheme="http://www.blogger.com/atom/ns#" term="eclipse" /><category scheme="http://www.blogger.com/atom/ns#" term="ides" /><title>Caught between Two IDEs</title><content type="html">&lt;p&gt;
I seem to be caught between two IDEs: Eclipse and IntelliJ. I abandoned Eclipse a couple of years back, partly based on wide spread recommendations from many different people, and partly because Eclipse just stopped working for me (it crashed out).

&lt;p&gt;
After I got started with IntelliJ I started to appreciate its merits, despite a generally clunky interface (with lots of modal windows), truly awful documentation. Many things are streamlined and only a ctrl-alt-shift-coke-bottle-touch-your-nose away.

&lt;p&gt;However, over time, using IntelliJ got slower and slower and slower. It also started running the Tapestry test suite horrifically slowly: 40 minutes and up (it should be about five). It would often &lt;em&gt;go away&lt;/em&gt;, even when memory wasn't tight. Indexing? Checking Repositories? Computing primes?  No way to tell.

&lt;p&gt;
Meanwhile, Eclipse has been moving forward, with Eclipse Galileo being a Cocoa (not a Carbon) application. Critical plugins such as M2Eclipse have gotten nice, and the Clojure plugin is mostly better than the IntelliJ one (though both are very early).

&lt;p&gt;
For a while I was using IntelliJ when teaching Tapestry (as part of the VMWare image I use when training) ... and I got a lot of resistance. People were much happier with Eclipse on the last couple of go-rounds, and I'm sticking with it.

&lt;p&gt;
Overall, I'm feeling that &lt;em&gt;most&lt;/em&gt; of what I've grown used to in IntelliJ is present in Eclipse, just handled a bit differently. The Clojure plugins are a wash; IntelliJ has the edge on the Git plugin. I think Subversion inside Eclipse is actually better.

&lt;p&gt;I've even cranked up NetBeans but didn't find anything there compelling enough to switch.

&lt;p&gt;
It seems like all my major tools (Firefox, Firebug, Eclipse, IntelliJ) are in the habit of growing too complex, and doing too much stuff in the background that I don't care about. All those intentions in IntelliJ that you have to turn off (for performance reasons), and all those extra plugins for Eclipse that you need to not download in the first place ... they're all getting in my way.  

&lt;p&gt;
I think a lot of this falls into the general category of accidental complexity ... to address the limitations of the Java programming language, all this extra stuff is coming into play: tools and wizards and plugins and indexes and whatnot. I find it pretty pleasant to work with Clojure instead, where the accidental complexity of Java is managed and isolated and the IDE doesn't feel the need to be overly ambitious. That's the Clojure concept right there ... grow the language to your needs, rather than building up tools. I think that's the Tapestry ethic as well.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/4110180-1178611148957672368?l=tapestryjava.blogspot.com' alt='' /&gt;&lt;/div&gt;&lt;img src="http://feeds.feedburner.com/~r/TapestryCentral/~4/7XUSHRp-H5o" height="1" width="1"/&gt;</content><link rel="replies" type="application/atom+xml" href="http://tapestryjava.blogspot.com/feeds/1178611148957672368/comments/default" title="Post Comments" /><link rel="replies" type="text/html" href="https://www.blogger.com/comment.g?blogID=4110180&amp;postID=1178611148957672368" title="11 Comments" /><link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/1178611148957672368?v=2" /><link rel="self" type="application/atom+xml" href="http://www.blogger.com/feeds/4110180/posts/default/1178611148957672368?v=2" /><link rel="alternate" type="text/html" href="http://feedproxy.google.com/~r/TapestryCentral/~3/7XUSHRp-H5o/caught-between-two-ides.html" title="Caught between Two IDEs" /><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/07/caught-between-two-ides.html</feedburner:origLink></entry></feed>
