dinsdag 26 augustus 2008

Template your way into the hearts of your users !

This week we discussed our communication strategy at work.  As already told previously we are writing a security service which involves creating users, adding and removing permissions and so on...  Any of these actions can be performed by various actors (the user himself, an administrator, some batch job, ....) and thus the user must be notified about any change!  Of course requirements are that the business should be able to change the emails, so hard coding them is out of the question (as always :-).

I had a look at a couple of template engines this weekend to solve the problem.  Since I used Stringtemplate before I decided to go along with that one.  The beauty of a template engine is that you can pass in an object graph and let the template figure out what to write and more importantly how to write it.  First task was to make a simple email template that could be sent to the user:

1 group TemplateEmail; 2 3 Email(person, action) ::= << 4 5 \<html\> 6 \<body\> 7 Dear <person.name>, 8 \<br\> 9 \<br\> 10 11 You receive this email because the following action \<b\> "<action.Name>" \</b\> has occurred. 12 \<br\> 13 \<br\> 14 15 This leads to changes in the permissions you have on our applications. 16 \<br\> 17 18 Please logon into our application to validate your permissions. 19 20 \<br\> 21 \<br\> 22 \<br\> 23 Sincerly, 24 Patrick 25 \</html\> 26 \</body\> 27 >>

Done, this template creates a personal email with a header including the user's name and a subject containing the action that has occurred.  Please not that the html syntax is entirely escaped because the < and > are used to refer to parameters and other templates.  According to the documentation you should be able to use $ as well, but in my template these templates were never replaced.  Is it because the dollar is that low?  :-)). 

The two templates that need replacing are <person.name> and <action.name>, the engine will try to find a property name on both of these objects (person and action), which need to be handed to the template engine.  (If it cannot find this, the templates results in an empty string).  Next we need to convert this template into a email with all values filled in, the following code does just about that:

1 var group = new StringTemplateGroup(new StreamReader("templates\\TemplateEmail.st")); 2 var template = group.GetInstanceOf("Email"); 3 4 template .SetAttribute("person", person); 5 template .SetAttribute("action", action); 6 7 var emailBody = query.ToString();

Again, piece of cake, if you put the result of the toString() into your email body you have nicely formatted email.  How did this work?  The first line reads the file from the disk in which the templates are stored.  You get a group of templates as result, you then ask it for your named template.  If you require attributes you can set them here by using the SetAttribute methods.  Finally let the template engine do its work by calling the toString() on it.

All right, seems that basic templating is easy, let's switch to the next gear and divide our template into several little reusable templates.  After all we will want to send different mails based on what action has been performed, the type of user who has performed the action, ....  So let us create some templates for a mail header, mail subject and mail footer:

1 WriteMailHeader(person) ::= << 2 3 \<html\> 4 \<body\> 5 \<div\> 6 Dear <person.name>, 7 \</div\> 8 \<br\> 9 \<br\> 10 >> 11 12 WriteMailAction(action) ::= << 13 14 You receive this email because the following action \<b\> "<action.Name>" \</b\> has occurred. 15 \<br\> 16 \<br\> 17 >> 18 19 WriteEmailFooter() ::= << 20 \<br\> 21 \<br\> 22 \<br\> 23 Sincerly, 24 Patrick 25 \</html\> 26 \</body\> 27 >>

As you can see all multi line templates start with the template name followed by the argument list.  The ::= << sign means the templates starts here and ends with the >> sign.  Our mail template can now be rewritten as follows:

1 group TemplateEmail; 2 3 Email(person, action) ::= << 4 <WriteEmailHeader(person)> 5 <WriteEmailAction(action)> 6 7 This leads to changes in the permissions you have on our applications. 8 \<br\> 9 10 Please logon into our application to validate your permissions. 11 <WriteEmailEnding()> 12 >> 13

The following step is to make our mail more useful by providing extra information.  Lets assume that our Person has a property AuthorizationSet which holds two collections (one for the applications you are authorized to use and one for the functionalities you can execute).  Our template can now read these properties and act upon these values.  So let's add the applications and functionalities you are authorized for in the mail template.

1 WriteApplicationInformation(person) ::= << 2 You have access to the following applications: 3 \<ul\> 4 <person.AuthorizationSet.Applications:{application|<WriteApplicationInformationLine(application)>}; separator="\n"> 5 \</ul\> 6 >> 7 8 WriteApplicationInformationLine(application) ::= << 9 \<li\> <application.Name> of type <application.Type> valid from <application.StartDate> till <application.EndDate> \</li\> 10 >> 11 12 WriteFunctionalityInformation(person) ::= << 13 You have access to the following functionalities: 14 \<ul\> 15 <person.AuthorizationSet.Functionalities:{functionality|<WriteFunctionalityInformationLine(functionality)>}; separator="\n"> 16 \</ul\> 17 >> 18 19 WriteFunctionalityInformationLine(functionality) ::= << 20 \<li\> <functionality.Name> with code <functionality.Code> valid from <functionality.StartDate> till <functionality.EndDate> \</li\> 21 >> 22 23

And you simply call these templates with the following 2 lines in your email template:

<WriteApplicationInformation(person)> <WriteFunctionalityInformation(person)>

Let's go a little bit deeper in the previous templates we've seen, shall we!  We have created 4 sub templates, two for application permissions (WriteApplicationInformation and WriteApplicationInformationLine) and two for functionality permissions (WriteFunctionalityInformation and WriteFunctionalityInformationLine).  The main template calls the line template for each item.  This is achieved by the following syntax: < property : { subtemplate }; separator = "\n">,.  This means that for each property (if it is an array, otherwise it will be called only once) the subtemplate is called, the "\n" character will be used in the template between every iterated call.  The subtemplate has the following pattern { parameter | <template(parameter)>}.  First you name the value for each iterated object, then you specify what other named template to call (or use an inline template).

Finally I would like to add extra information on the items depending on the value we have in the permission sets.  Every permission has a start date and an end date.  These are shown as well, but now when a permission has an end date set to sometime far in the future, it doesn't really add any extra value.  On top of that it looks unprofessional too, so let's us vary the template based on wether we have a permission that is open ended.  First add an extra property in your permission class IsOpenEnded that returns true/false based on the EndDate == DateTime.MaxValue.  Then make an if structure in your template depending on this property.  If we have an open end, we simply make do not display the end date, but show a slightly different text.

1 WriteApplicationInformationLine(application) ::= << 2 \<li\> 3 <application.Name> of type <application.Type> 4 <if(application.IsOpenEnded)> 5 valid from <application.StartDate> till piggs can fly 6 <else> 7 valid from <application.StartDate> till <application.EndDate> 8 <endif> 9 \</li\> 10 >>

So now we have a nice looking email that communicates everything we want with just providing an object tree to our template.  But of course that doesn't suffice.  We want to be able to differentiate templates between the action we have performed, that was one of the reasons why we created the small templates remember.  So if we modify the code where we load our template, we can choose a different template based on the action we had.

1 var group = new StringTemplateGroup(new StreamReader("templates\\TemplateEmail.st")); 2 var template = group.GetInstanceOf("EmaiForl"+action.Type); 3 4 template .SetAttribute("person", person); 5 template .SetAttribute("action", action); 6 7 var emailBody = query.ToString();

This means we need to have templates for the different actionTypes our application has, in our case these two templates would be required:

1 EmailForChangeAuthorization(person, action) ::= << 2 <WriteEmailHeader(person)> 3 <WriteEmailAction(action)> 4 \<br\> 5 This leads to changes in the permissions you have on our applications. 6 \<br\> 7 <WriteApplicationInformation(person)> 8 <WriteFunctionalityInformation(person)> 9 10 <WriteEmailEnding()> 11 >> 12 13 EmailForChangePassword(person, action) ::= << 14 <WriteEmailHeader(person)> 15 <WriteEmailAction(action)> 16 17 We are glad to inform you that your new password is <MakeBold(person.password)>, please 18 logon and change your password to something more personal. 19 20 <WriteEmailEnding()> 21 >>

 

That pretty much it.  If you want to have a look at it yourself, you can download the code or go have a look at the he Stringtemplate website.

2 opmerkingen:

Unknown zei

How about NVelocity?

Patrick zei

Had a look at nvelocity as well, but I found Stringtemplate to be easier and less code.