torsdag den 10. april 2008

Tapestry trick for caching autogenerated images

At work I am currently using Tapestry 5.0 to create a portal for the game we are working on "Call of the Kings". This has given rise to a lot of stress, but working with it more and more convinces me that it was a good choice. I should warn though that it is not a flat learning curve - consider it more like a wall that can be climbed if you try very hard. Once climbed it will grant you enormous potential.

The trick I am going to talk tell about it how to make caching for generated images/pages/etc. where you know that the only thing affecting them is the request-parameters (called the context in Tapestry5.0 language, available during "Page Activation").

First of all you must understand that the context for a tapestry page is encoded as "subfolders". Lets say that the context expects 2 integers and a string, then it would look like this:


http://localhost/yourproject.isnice.com/pagenameinlowercase/42/32/thestring


Now this is VERY nice for bookmarking, it is nice for remembering, it is fun when testing etc. It is however very different from what we are used to. Also it leads to the following question:

What if I had a directory structure like this "pagenameinlowercase/42/32/" and inside the top folder had a file called "thestring" what would Tomcat/Jetty/etc. do with the request?

Jup, it would render the static file in preference of calling your onActivate(42, 32, "thestring") on your page called "PageNameInLowerCase". We will come back to this later.

Now these parameters are all of type string to start with so Tapestry uses something called a "Coercer". This you have to inject using IoC into your page - don't ask that is the wall you will climb. Once you have this little thing going it's easy to convert between types (and using IoC you can even add more type coersions for your own types).

Ok, that was a lot of stuff with no real point - so here goes the actual case:

We have a heraldry generator in our game, it takes some base images and combines them into flags/shields etc. Only one player can have any given combination, but since you choose 4 images and we have a lot of them I guess there are a few billion combinations. Now these images are used a lot on the portal to identify players, but only few of the possible combinations are actually used. Therefor we would like to generate them on the fly - but as the promise stated, this is a case where ONLY the request-parameters (Tapestry: context) have influence on the result.

Now, the shields we generate take 4 integers and a size (since we also want to scale them). So a request to our images generator-page looks like this:


http://portal.cotk.net/showheraldryshield/shield/301050/301000/200782/110881/64/76


Now the first parameter "shield" is just to tell it that we are going to need a shield (not a flag or something like that). The next 4 are the actual heraldry components and the last one is the size. Now the trick is that in our component we can easily access these parameters and convert them to integers. It looks something like this:


@OnEvent(value="activate")
public StreamResponse onActivate(Object[] context)
{
String type = (String)context[0];
if("shield".equals(type))
{
return _heraldryImageProvider.getHeraldryShield(
_typeCoercer.coerce(context[1], Integer.class),
_typeCoercer.coerce(context[2], Integer.class),
_typeCoercer.coerce(context[3], Integer.class),
_typeCoercer.coerce(context[4], Integer.class),
_typeCoercer.coerce(context[5], Integer.class),
_typeCoercer.coerce(((String)context[6]).replace(".png", ""), Integer.class)
);
}
else
{
return null;
}
}


Basically we convert parameter with indexes 1-6 to integers... But what is that replace ".png" thing at the end?

Yes, that is the trick, since if the request had looked like this:


http://portal.cotk.net/showheraldryshield/shield/301050/301000/200782/110881/64/76.png


The last parameter would not have been "76" but "76.png". It is very smart that I strip off this part, because now when generating a shield that I have not generated before I can just store it as a plain png-image on the server. Next time someone requests this specific heraldry (and they will since I intend to keep it) Tomcat will serve them the stored image.

Conclusion
Just by taking advantage of a parameter (Tapestry: context) encoding we can implement caching just by placing the result at a location that would correspond to the first request. Then Tomcat/Jetty/etc. will serve the cached version. This could be used for HTML/JS/Jpeg/SWF/etc. but in case the result is something a little more "live" than an image you should watch out that you don't give your visitors an easy injection-point on your web server. But in general this falls under the normal "Watch out what you do with arguments given to you by a user"-paranoia that one must have when dealing with a server-application.

Ingen kommentarer: