A Year of Tapestry

By Taylor
Filed Under Software 

Well, the blog is up and running.  Lots to do on it, but first and foremost is to get some stuff off my chest.

I’ve been working for a software startup for a little over a year.  I was brought in to help rebuild their product, a professional collaboration tool built in Java on top of a framework called Tapestry 5.

I’d heard of Tapestry before, but hadn’t paid much attention since the Java web framework space is so dominated by Spring.  Tapestry has been around for a while, but their latest version is a fundamental re-imagining.  Tapestry also has the distinction of being Apache’s recommended java web framework, a spot formerly held by the venerable Struts framework.

Having never used Tapestry before, and being familiar with both Spring and Struts, I can safely assert that I’m fairly objective.  I like Spring, but by no means do I carry a torch for them.  Many of the comparisons that follow are to Spring, since Spring is the defacto standard.

A good initial read on Tapestry is this InfoQ article, but a hefty pinch of salt is in order.   My opinions of Tapestry are decidedly more mixed.  Read on for details.

The good:

  • Basic IOC is elegant
  • Integration with Prototype, Scriptaculous is clean
  • Blackbird console is useful
  • Useful ui components for simple cases
  • A supportive (if small) set of primary participants on the mailing list

The bad:

  • Limited documentation
  • Spotty support, limited thought to people actually trying to run sites in the framework
  • Reinventing the wheel, only square – in many cases Tapestry has rebuilt, in a weaker fashion, what already exists elsewhere
  • Misleading/inaccurate statements about REST support

The ugly:

  • Transaction support is lame, minimal
  • 3rd party packages/integrations are few, and those that do exist are prototype-grade at best
  • Web tier is clunky, cumbersome in more advanced situations
  • Who the hell made everything final?
  • Serves up any file in the deployment, by default

I’m not going to talk too much about Tapestry’s strong points, since they and others talk about them quite well already.

The documentation for Tapestry is limited compared to Spring.  Some of this is no doubt an effect of team size and usage.  However, there are times when Tapestry documentation spends time extolling virtues of a given approach, rather than substantive examples.  The examples that are provided often gloss over details.  Overall, I found tapestry difficult to approach for this reason.  It’s hard to know what one part of the documentation is talking about because it assumes you know other Tapestry bits and refers to them in an offhand manner.

The support in Tapestry is essentially non-existant.  Once a final feature version (aka “dot-release”) is cut, no support releases are issued.  Case in point, the “stable” release of Tapestry at time of writing uses a version of Prototype that does not support IE8.  The newer version is incorporated into their trunk, but this is still in dev state, and there’s no fix indicated for the stable release.  If you have a production app and were hoping to support IE8, you’re SOL until the next Tapestry feature release.

Additionally, features get released in a “half-baked” form.  For example, a feature in the most recent feature version (5.1) of Tapestry is the ability override bean definitions.  The problem is that this circumvents all interception.  If one has a bean defined and decorated with interceptors (e.g. transactional interceptor), and one overrides the bean impl, one loses all interception.  The only work around here is to provide the overriding bean as a non-delegating (i.e. impolite) “interceptor”.  This is a hack that is very brittle to things like interceptor ordering.  I logged a bug about this some time ago, no movement, no comment.

Tapestry, as a framework, necessarily must do what many other frameworks do.  This has led to some interesting and novel approaches.  However, some of Tapestry’s implementations are severely lacking.  Take, for example, their decorator/interception model.  It involves an excessive amount of code, and going through the guts finds some inefficiencies (e.g. AbstractInvocation.getParameterCount() forces an array copy).  Further, many things are built in an almost xenophobic fashion.  There is no easy way to get the java.lang.reflect.Method from Tapestry’s interception framework (stunning since their Invocation impls all wrap Method objects, and expecting a java.lang.reflect.Method doesn’t seem a stretch or implementation specific).

It would seem they would have been better off to implement AOP Alliance than to home brew here.

Tapestry has also stepped away from the traditional MVC approach; it uses page beans instead of controllers.  Many claim that this is more intuitive, since now you can store state in the page.  I realize that JSF and others are doing this too, but I just don’t get it.  Why have services as singletons, and pages aren’t?  This strikes me as counter-intuitive but maybe this is just a subjective thing.

Regardless, this leads to a problem. Firstly, from the Tapestry site:

This is necessary for several reasons, most importantly because Tapestry pages are pooled. Creating a Tapestry page is an involved process, because the page object is simply the root of a large tree of other objects including user provided components, many kinds of structural objects, template objects, and others. Creating a new page instance for each request is simply not scalable.

This seems to imply that Tapestry is unable to handle high concurrency.  500 people viewing the same page at a time means that 500 page beans, and associated object trees must exist.  If the creation is such an involved process, how to handle spikes on accessing pages?  Tapestry has limiting built in, such that eventually, if too many people access a page at a given time, it stops creating new page beans and starts chucking errors.  I’ve seen Tapestry get to this state but I haven’t seen it recover (granted, I haven’t thoroughly tested this aspect, but it’s concerning nonetheless). Further the idea of “fixing” singleton controllers by imposing a cap and then throwing errors when that cap is exceeded strikes me as a cure that’s worse than the disease, for high-traffic sites at least.

The default limit per page is 30.  This configurable both globally and by page, but excessive object creation per thread means that even if one configures a huge limit, at some point all that has to be garbage collected.  The singleton approach (and I realise singleton is a hot-button issue, I don’t really want to get into a singleton debate here anyways) has this much going for it; it’s incredibly scalable.

Tapestry has made claims that it offers RESTful URLs.  There are several problems here.  Tapestry’s urls tend to follow the following pattern:

http://domain:port/folders/page:event?t:ac=something

Some of the above are situational, but the bits that start to cause problems are the page:event and t:ac parts.  I’ve read the RFC for URI’s, as far as I can tell a colon is allowed in two spots, to denote the protocol (e.g http://) and to separate the domain and port.  The colon separating the page and event, as well as the “namespaced” param violate the spec as far as I can tell (please correct me if I’m wrong).  Browsers don’t seem to mind the colon, but flash sure does, at least in AS2.  To “trick” flash into pulling a url with a colon you have to double-encode it.

Further, looking at the resultant HTML source for a Tapestry form shows something odd.  A hidden field that looks like the following (line breaks added by me):

<input name="t:formdata" type="hidden" value="H4sIAAAAAAAAAJXOM
Q4BQRSA4WcT1aokbkB0sw0N1YaoRCQbB3gznjXs7pvMDNZlnEBcQqFzBwfQqhS2
UGi1X/In//kJ9UMXOh6N4txE8QbL0ZrUVnI5ppwHWIH6grPQZ5sKNFiRqBpy3h7
7QrGlTEsh0ZGIZYWo/ERTtmwn5Hems7iGj9btHUBtCqHiwlvOZpiTh+Z0g3uMMi
zSKPFWF+mwNB4a+DPyz2D87+DcsiLnkp3MtXOai+tl2Vu9TvcAoDQfKFr/PiAB
AAA="></input>

And this is for a fairly simple form.  A complex form can lead to pages of gunk.  What is that?  Turns out it’s state and field binding information.  Eep!!  Aside from the obvious cludge of sending data from the server to the client, simply to have the client send it back to the server, isn’t there a security risk here?  Yes, but don’t worry, it’s hard to hack, we’re told:

Although you could use this technique (severe hacking of t:formdata) to control what ComponentAction was instantiated at what point in the form submission, the security effects of this are minimal; Tapestry includes only a finite set of ComponentAction classes and each has a very specific job; the worst that you could do would be to redirect certain property updates to certain other fields, and doing even that would require deep understanding of Tapestry and of the specific application.

Above was posted by Howard Lewis Ship, creator/lead of Tapestry.  The barrier to exploiting this is a) knowledge of Tapestry (open source software) and the target application.  Hence your real barrier is knowledge of the target application, but since many hacks are carried out by or involve insiders, this is not reassuring.  As Ivan Dubrov points out further down the list,

security by obscurity” [is] not [a] very good approach to secure code.

Ivan is spot on. This is concerning and Tapestry provides no means to suppress or replace this behaviour with something more secure.

In addition, requiring state information in any post is hardly RESTful.  To create a thick client that leverages web endpoints, the client must first load up a page, suss out all instances of t:formdata (there can be more than one in may forms), and then include that in its request.

The transaction support is one of my biggest pet peeves about Tapestry.  Any of us who have worked on complex apps that involve lots of data modification know the necessity of concise, well defined transactions.  The common definition of a transaction is a unit of work, but this can be a bit vague.  I like to think of them as the following: “it all passes or it all fails”.  Consider a transaction that both updates a record and performs audit trail logging into the db.  If the audit trail logging fails, we probably don’t want the change going through anyway.

In Tapestry, one has two choices; manual transaction management or use of their @CommitAfter annotation.  A service can have one or more methods annotated with @CommitAfter.  An interceptor then commits the current txn and starts a new one whenever a method with that annotation is exited.

In a situation where one annotated method calls another, when the second method is completed, all work is committed, and a new transaction is started.  Any subsequent work is in a new transaction, and if anything fails that first transaction is already committed in the db, there’s no way to call it back.  This leads to either brittle transactions or situations where service have two methods per logical method, one transactional and one not.  This makes reuse of service logic difficult and leads to code bloat.

The available 3rd party packages range from successful to mediocre.  Tapestry has successful integrations with Spring IOC and Hibernate (barring the transaction stuff described above).  However, outside of this, integrations are limited and when available, poorly implemented and supported.  The ChenilleKit package, for example, provides many enhanced widgets and integrations with packages like Opensymphony’s Quartz.  However on closer examination, these do not hold up.  Many controls fall apart in loops or AJAX contexts, and the Quartz integration works only with an in-memory job store, and provides no out-of-the-box integration with the Tapestry IOC.  In other words, if you want your jobs to talk to your services, you’re SOL.

While this isn’t something the Tapestry team can fix, it causes many problems for someone trying to build on top of Tapestry.  In these days of Ruby on Rails and Grails, the only reason to pick Java is the plethora of API’s available.  The slim pickins here hurt Tapestry’s ability to stay agile.

As mentioned previously, Tapestry take the page bean approach.  This can lead to pretty cumbersome pages as Tapestry’s expression language for their .tml’s  (.jsp analogues) is weak on anything more than data retrieval.  The recent 5.1 release took steps to improve this, but tracing data flow in a page often involves lots of back and forth between the .tml, and various methods within the page bean.  Additionally, all the getters and setters lead to very long page beans in terms of lines of code.

The default behaviour of Tapestry is to serve up any files with a couple of minor exceptions, from the deployment.  It will even serve up files out of jar’s and in 5.1 hands out directory listings.  This is done to ensure that plugins can be packaged in jars and still access assets like css, js, etc… they are packaged with.  It will also hand out any file in the deployment, including anything in the WEB-INF folder (so much for the servlet spec).  I would hazard that most sites out there running Tapestry 5 and using Hibernate are merrily serving up hibernate.cfg.xml(this usually includes a db name and password), none the wiser.  Our application isn’t but it was for a while.

Tapestry has mitigated this by allowing you to blacklist files by pattern, and defaults to .jar and .class files.  Still, this is stunningly insecure, and a serious trap for anyone who chooses to develop on Tapestry.  Further, this is discussed only in the mailing lists, so if one followed tutorials, without arbitrarily trawling the mailing lists, one’s fly is decidedly down, so to speak.

A lot of the above could be worked around, but Tapestry, and ChenilleKit by extension, make many classes final, and use runtime-generated bytecode.  This makes overriding or extended impossible in many instances.  This is done because previously users had complained when the framework changed and their extensions didn’t work, so they’re telling you what should and should not be extended.  However, it’s cold comfort when you’re facing a deadline, and have to re-implement a component in order to insert a two-line fix that would have been trivially easy with an extension.

End of rant.

I realise I just dumped hugely on Tapestry, and perhaps unfairly, I didn’t spend much time highlighting the good points. Overall though; Tapestry seems to have tripped over itself.  The elegant fundamentals of the IOC are overshadowed by the cumbersome aspects of its transactions and integrations, a highly dangerous security vulnerability, the clunky feel of the web tier, and the overall lacking support.

If you must go with Tapestry, consider using something like this tutorial, where the service layer and transactions are managed via Spring.  In such a case, however, what is one then using Tapestry for?

Tapestry hasn’t found it’s niche.  Ruby on Rails and Grails have it trumped for velocity, and Spring dominates in support, maturity and API availability.  While it does decently on several fronts, key problems keep it from being the best at anything.  Hence, regardless of what your priorities are, I wouldn’t recommend it as a platform.

Comments

Leave a Reply