Tapestry Training -- From The Source

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

Wednesday, August 29, 2007

Handling direct URLs in Tapestry 5

So, I'm starting work on the 1% web portion of my otherwise 99% Swing project. It's about sending invoices by email and allowing external users to view invoice PDFs, and accept or reject them.

I wanted to get a tracer bullet going; the process is kicked off when some COBOL code, via typical Rube Golderberg shenanigans, GETs a URL of the form /sendinvoice/invoice-number/email-address.

Well, in Tapestry terms, that could be a page with an activate event handler method:

package com.myclient.pages;

import org.apache.tapestry.util.TextStreamResponse;

public class SendInvoice 
{

    Object onActivate(String invoiceNumber, String emailAddress) 
    {

        // TODO: Generate and send the email
        
        return new TextStreamResponse("text/plain", 
          String.format("OK %s / %s", invoiceNumber, emailAddress));
    }
}
Returning a StreamResponse (that's the interface, TextStreamResponse is a simple implementation) allows your code to bypass the normal HTML response with one you control. This kind of thing was way too complicated to do in Tapestry 4 so I'm very happy with how minimal the Tapestry 5 approach is.

I fired this up and it worked the first time, echoing back to me the information I passed in the URL. If you don't provide at least two values in the URL, Tapestry will bypass this activate event handler method and will just render out the page; the page template simply announces to the user that they munged up the URL in some way.

This is something that would be pretty easy as a servlet, but is even easier in Tapestry. Just create the SendInvoice page and provide the onActivate() method. onActivate() gets called based on the number of path elements provided in the URL. When there are two (or more), this method gets invoked with the first two. Tapestry can automatically coerce from string to other types, but here both values happen to be strings anyway.

Obviously, much more to do (under deadline ... I don't really have time to blog this). I'm just happy to be doing a little Tapestry on my day job

7 comments:

Massimo said...

So are you telling us this is the first time T5 jump into your day to day job?

No, it couldn't be that case :)

Carlos Delfino said...

Hi Howard!

With tapestry is possible I get parameters in URL with a MapCollection?

See this URL: https://www.carlosdelfino.eti.br/SendInvoice?InvoiceNumer=123456&EmailAdress=fulano@domain.com.br

code:

package com.myclient.pages;

import org.apache.tapestry.util.TextStreamResponse;

public class SendInvoice
{

Object onActivate(Map p_map)
{

// TODO: Generate and send the email

return new TextStreamResponse("text/plain",
String.format("OK %s / %s", p_map.get("invoiceNumber"), p_map.get("emailAddress"));
}
}

Howard said...

Nope, that's not supported.

You can get query parameters by injecting the Request.

The context is explicitly defined as the extra information in the path after the page is identified.

This is very much on purpose as it results in shorter, more readable URLs that are more attractive to users and more navigable by web spiders such as search engines.

sicklittlemonkey said...

The onActivate paradigm you've set up is certainly very convenient to use - except when multiple handlers are called. I've logged a JIRA.

Anyway, glad you're getting to enjoy your framework a bit like everyone else is. ;-)

Cheers,
Nick.

Marcus said...

Hi Howard,

A common use for this method is security, something like:

Object onActivate()
{
if (!_visit.isLogged())
return "Login"; // Deny Access

return null; // Allow access.
}

In that cenario, how we can do the both things ? (context activation and security access)

If there is two methods, one like you show and other like this, Tapestry will call both?

Thank you!

Howard said...

This isn't well documented, unless you know where to look.

When multiple methods handle the event (which kind of assumes you are using the @OnEvent annotation) they are called in alphabetical order.

When a single method is overloaded with different parameters, the order of invocation is fewest parameters to most parameters.

So onActivate() will be invoked before onActivate(String, String).

Chris said...

In response to Marcus' example of using onActivate() for implementing security - I'd like to suggest that such a use a Bad Idea (tm). Yes, it could be done this way and yes, it could accomplish your goals, but at a high maintenance cost. With such a setup you have 2 ways to secure pages:

1) Implement security controls on every page that needs them.
2) Implement security controls in one (or more) base page, and then extend it for each page so the parent's restrictions can be impose on the child.

Imagine a site with 20 pages. Yuck. 80 pages. Sick. 100+.... yeah.

Now throw in the complexity that several pages may have more exotic security needs. You get the idea.

I'm commenting to suggest the idea of using Dispatchers to implement security. Longer story short, you can implement controls in a central location - once - and the pages are completely ignorant of it. In my opinion this is near the holy grail of access security.

I've written a wiki about the foundational work needed here. There's nothing complicated - all ideas come from looking at how T5 itself intercepts and processes requests.

Of course if you're familiar w acegi, and I'm not, then that's probably a suitable route. What I want to point out is that implementing security page by page in onActivate is a bad idea in the long run.