Developers
AvantGo Channel Developer Guide

TOC PREV NEXT INDEX


Cookie personalization example: a Movie Review Channel

So with our knowledge of cookies, we will tackle a typical example of a personalized channel. We will also need to draw on some information about forms, which are covered in detail in CHAPTER 7. Managing channel form submissions.

Suppose you were creating The Movie Review Channel, a (fictional) channel that provides new DVD, video, and theater reviews daily. It tends to be a lot of information and the reviews can be pretty long-winded. So it would be nice if you could pre-screen them and only present the ones the user is interested in. Maybe your reader is into action flicks. Or comedies. And maybe the idea of seeing a family-oriented kiddie film makes him break out in a rash.

What you would like is the ability for any user to customize his movie preferences through a form submission. He could go to a form, select the movie genres he prefers, and the next time he syncs, he would be presented with a list of movies that fit into one of those genres.

Design concept: user ID in cookie, the rest in a database

Here is the high level, birds-eye view of the process involved in creating this channel:

You create a cookie for the user that contains a the user's unique ID. You keep a database on your server that contains a list of every user, along with his genre preferences.

When the AvantGo sync server accesses The Movie Review Channel's front page, you would look up the cookie for the user (retrieved from the AvantGo sync server) to obtain his ID. You can use that ID to look up a list of his preferences and generate a custom list of movie reviews for him to read.

Figure 6-3 ID in cookie used to look up preferences and generate page

What if he wants to change his preferences? You can provide him with a form that allows him to check or uncheck his movie preferences. This form, once submitted, will alter his preferences stored on your database.

Figure 6-4 Form for user to change preferences

What if this is his first time visiting our channel? We can also determine that by a cookie; specifically, the lack of one. In that case, it is often a good idea to present your user with some default content, along with a prominent message telling him to customize his preferences.

Figure 6-5 Setting cookie on user's first visit

Here are a couple of pages of the Movie Review Channel in action. Figure 6-6 shows the default list of movies that is displayed when user first visits the site, before a cookie has been set up.

Figure 6-6 Default home page displays default movie list

Figure 6-7 shows a form on which a user can select custom categories that the user is interested in.

Figure 6-7 User selects custom categories

Figure 6-8 shows a standard form submission dialogue box, presented when user taps Submit. (we will improve on this later).

Figure 6-8 Standard form submission dialog box

Figure 6-9 shows the customized Movie Channel home page, generated by using ID in cookie to look up user preferences in a database.

Figure 6-9 Customized Movie Channel home page

Keeping page requests in sync with preferences

Now, hold on a sec. Assume I change my preferences and then sync this channel. At the moment I sync, I will be performing two activities at once. I am going to be submitting the form to change my preferences, but at the same time, I am also going to be grabbing the front page of my channel. How do I know the front page is going to be based on my new preferences instead of my old ones?

Good question. When a form is submitted through the AvantGo sync server, we make sure that all the forms have finished submitting information and all the actions called by those forms have run to completion before we start requesting any pages from that channel.

OK, so you know the basic strategy for approaching personalized channels. The next section takes a closer look at implementing the channel.

Implementation details for this example

If you are comfortable using cookies and the strategy outlined above, you can safely skip the rest of this chapter and move on to the issue of managing the forms your channel users submit — see CHAPTER 7. Managing channel form submissions. If you really want the nitty-gritty details of how to implement a personalized channel, read on.

But before we do, a couple of quick disclaimers. First, the Legal Notice of Liability:

The information in this document is distributed on an "As Is" basis, without warranty. While every precaution has been taken in the preparation of the tutorial, neither the author nor AvantGo shall have any liability to any person or entity with respect to loss or damage caused or alleged to be caused directly or indirectly by the instructions contained herein.

And on a more personal level, I would like to mention that the code examples you are about to see should be used as general guidelines only. While the channel works, it probably is not as efficient or as bulletproof as something you could write yourself. So feel free to look at what I have done here and follow the same processes for your channel, but do not reuse my code. I am sure you can do better starting from scratch.

All code was created with Perl 5.005 with the CGI and CGI::Cookie modules (although I do not really use them to their full extent). I also made use of Perl's DBM capabilities, which gives me access to a very simple database. I am running these scripts on an Apache server under Windows 2000, although I am not using mod_perl, or any higher level tools like Mason, PHP, or ASP.

Perl can sometimes be a confusing language for those not familiar with it, but I tried to avoid some of the crazier idiomatic tricks. Hopefully, if you have some programming experience, you will be able to follow along.

Setting up the databases

I have created two databases here (they are both simple DBM files). One is a table of users. It contains a Unique ID number, the user's first name, followed by a list of 11 true or false values depending on whether the user likes that corresponding genre or not. Note that simple DBMs only allow one field per record, so my first name and 11 preferences are actually crammed into one large text string.

The other DBM file is a table of movies. It contains a Unique ID number, the name of the movie, followed by a list of 11 true or false values depending on whether the movie belongs in that genre or not. I created the table this way so that a movie can belong in more than one genre (For instance "GalaxyQuest" can be considered both Sci-Fi and Comedy.)

Figure 6-10 Current, inelegant database structure

Note: In my current implementation described above, I have to be very careful in ensuring that all the lists of 11-true-or-false values are always in the correct order. Plus, it makes it difficult to add or remove genres later. There are better ways of implementing this through a relational database such as mySQL. But this will do as an example.

Listing 1: frontpage.pl

This section analyzes the key parts of the sample source code for generating the front page. For a full listing of this source code, see frontpage.pl, basic version.

First, we load some Perl modules, and print out a standard HTTP header (along with a Cache-Control:private header — see the next section about why we do this.)

#!D:\Perl\bin\perl.exe

use CGI qw(:standard);

use CGI::Cookie; print header(-'Cache-Control'=>'private');

Next, we retrieve any cookies associated with this page.

%cookies = fetch CGI::Cookie;

If we find a cookie marked moviereviewID, then we open our database, look for a user with that ID number, grab his first name into the variable propername and his movie preferences into an array of 11 true-or-false values called prefs. (Again, remember that all my values are stored in the database as one large string with different fields separated by ~:~ characters. If I were using a more sophisticated database, I could go with something more elegant.)

if ($cookies{'moviereviewID'} ) {

  # If a cookie exists, we get its value

  # (which is the user's ID number)

  

   $myID = $cookies{'moviereviewID'} -> value;

  

   dbmopen(%prefs,"movieuserprefs",0644)

      || die "Can't open movieuserprefs! $!";

  

   # We look up the user's preferences based on

   # his ID within the database.

   #

   # The record itself is a big string of values

   # separated by ~:~ characters. The "split"

   # function will break upthat string and place

   # the resulting values into the variable

   # "propername" and the array "prefs"

  

   ($propername, @prefs) = split(/~:~/, $prefs{$myID});

  

   dbmclose(%prefs);

  

} else {

  

If such a cookie does not exist, we load up some default values and note that we are just loading default values.

# We make prefs an array of default values if

   # the user's cookie does not exist.

    @prefs = (1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0);

  

   # This is a flag that indicates we're using

   # default values.

  

  $default = true;

}

  

We print out some standard text for the beginning of the web page.

print <<END_of_Start;

<HTML> <HEAD>

<TITLE>Movie reviews</title>

<META NAME="HandheldFriendly" content="True">

</head>

<body>

<Center><h2>The movie review channel!</h2></center>

  

END_of_Start

  

print "<p>Welcome, $propername </p>" unless $default;

print "<p>Here are a list of movies you might be

  interested in...</p>\n <ul>\n";

  

This is where the work really happens. We go through every movie and check the genres it fits into. If we find it belongs to one of the genres that our user has selected on his list, then we print a link to the movie with the URL of reviews/<movieIDnumber>.html.

dbmopen(%movies,"moviereviews",0644) || die "Can't open moviereviews! $!";

# We iterate over each movie ID within the moviereviews

# database. foreach $filmnumber (keys (%movies)) {

# Once again, breaking up a string into the variable

# "filmname" and the "genres" array by using a split

# command.

($filmname, @genres) = split(/~:~/, $movies{$filmnumber});

$match = 0;

for ($i =0; $i < 11; $i++)

    { if ($genres[$i] && $prefs[$i])

    { $match = 1; # We've found a match! last;

                  # Break out of the loop.

                  # no need to keep looking.

    }

    }

# If this film has a match, include a link to it.

If ($match) {

    print "<li><a href=\"reviews/${filmnumber}.html\"> $filmname</a>\n";

    }

} dbmclose(%movies);

  

print "</UL>\n<p>Tune in next time for more movie reviews!<p>\n";

  

We include a link to the personalization page, and some encouragement for users who have not set up their cookies yet.

If ($default) { print "<center>Want personalized content? Of course you do!\n"; print "<br><a href=\"/cgi-bin/movies/editprefsform.pl\"> Edit your preferences</a></center>\n"; } else { print "<a href=\"/cgi-bin/movies/editprefsform.pl\"> Edit preferences</a>\n"; } print "</body>\n</html>\n";

  

This will present a front page that looks a little something like this:

Figure 6-11 Default home page produced by frontpage.pl script

Listing 2: editprefsform.pl

For a full listing of this source code, see editprefsform.pl, basic version.

Suppose our user wants to set or alter his preferences. All we really need to do is offer a form consisting of several checkboxes (one for each genre) plus a text field for his name. This could be done with a standard HTML page. But we want to make sure the initial state of the form represents the user's current choices. (In other words, if our user has already asked to receive "comedy" and "drama" reviews, the "comedy" and "drama" fields should already be checked.) So once again, we are creating a script to produce this form.

We start with the standard headers.

#!D:\Perl\bin\perl.exe

use CGI qw(:standard);

use CGI::Cookie;

  

%cookies = fetch CGI::Cookie;

  

If a cookie for this person already exists, we use that information to grab his name and preferences from our database as before.

If ($cookies{'moviereviewID'} ) { $myID = $cookies{'moviereviewID'} -> value; dbmopen(%prefs,"movieuserprefs",0644) || die "Can't open movieprefs! $!"; ($propername, @prefs) = split(/~:~/, $prefs{$myID}); } else {

  

If no cookie is available, we could give him an entirely blank form, or present him with a form where the default categories are already checked. This is a matter of taste, really. I chose to go with the default values already checked.

@prefs = (1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0); $default = 1; }

  

Print out some HTML:

print header(-'Cache-Control'=>'private');

  

  

print <<END_of_FormTop; <HTML> <HEAD> <TITLE>Get customized</TITLE> <META NAME="HandheldFriendly" content="True"> </HEAD>

  

<BODY> <p>Please indicate what movies you would like to receive reviews for:</p> <form name="choosemovies" action="/cgi-bin/movies/setprefs.pl"> <p>

  

END_of_FormTop

  

Now, we print out a checkbox for each genre category. If we find that, according to our user's preferences, he is already subscribed to that category, we mark it checked by default.

@genrenames = ( "Action/Adventure", "Art House/Indie", "Classics", "Comedy", "Drama", "Horror and Suspense", "Kids and Family", "Musicals", "Sci-fi/Fantasy", "Special Interest", "TV/Documentary" );

  

for ($i = 0; $i<11; $i++) { print "<input type=\"checkbox\" name=\"$i\" value=\"1\""; if ($prefs[$i]) { print " checked"; } print ">\n$genrenames[$i]<Br>\n"; }

  

Do something similar with the name field:

print "<p>\nPlease enter your first name.\n"; print "<input type=\"text\" name=\"firstname\" size=\"15\" maxlength=\"15\" value=\"$propername\">\n </p>\n";

  

And we are done, except for a little clean up work. Note the Submit button right now looks like every other submit button you have seen before. We are actually going to change this in the next section, so keep that in mind.

Print <<END_of_Page;

  

  

<input type="submit" name="submit" value="Submit">

  

</form> </BODY> </HTML>

  

END_of_Page

  

This will give us a checklist that looks a little something like this:

Figure 6-12 Form produced by editprefsform.pl script

Listing 3: setprefs.pl

For a full listing of this source code, see setprefs.pl, basic version.

All we need now is a script to process the results of the form we generated above. In other words, we need to set a cookie for the user, if one is not set already, and edit his preferences in our database.

We start off again with the usual headers (plus an srand call to start up our random number generator).

#!D:\Perl\bin\perl.exe use CGI qw/:standard/; use CGI::Cookie; srand;

  

Then we grab the parameters returned by the form. Note that all of our genre preference parameters within the previous form are named "1", "2", "3", and so on. So it is relatively easy to grab their values from a for loop.

We put the user's name into a variable called username and his preferences into an array of 11 true-or-false values. That last line there replaces any ~:~ strings with ~;~ in case somebody happened to include our record-separating string within his name. (Although that would be a really strange name!)

for ($i=0; $i<11; $i++) { $prefs[$i] = param($i) || "0"; } $username = param("firstname"); $username =~ s/~:~/~;~/g;

  

If the user's cookie indicates he has an ID already, we use it. Otherwise, we generate one using the getrandomID function — this is a function created below that generates a random ID for us.

%cookies = fetch CGI::Cookie; if ($cookies{'moviereviewID'} ) { $myID = $cookies{'moviereviewID'}->value; } else { $myID = getrandomID(); }

  

Then we use Perl's CGI::cookie function to create a cookie. I have set the expire time to 10 years from now, since we really do not want our cookies to expire between syncs. (If I wanted to be thorough, I could also define my own domain and path values for the cookie.)

$tkcookie = new CGI::Cookie(-name=>'moviereviewID', -value=>$myID, -expires=>'+10y');

  

We write the user's preferences into the database, joining them together as a ~:~-separated list of values.

dbmopen(%prefs, "movieuserprefs", 0644) || die "cannot open DBM $!"; $prefstring = $username . "~:~" . join ("~:~", @prefs); $prefs{$myID} = $prefstring; dbmclose(%prefs);

  

Finally, we print out our HTML document. Note the set-cookie line among the HTTP headers. This document will be placed in the Forms Manager during the next sync. It is really just a bunch of debug information, and I do not really want our users to see it. (After the next sync, our users will be looking at the new frontpage.pl.) In the next section, we will find out how to hide this from the Forms Manager.

Print "Content-type: text/html\n";

print "Set-Cookie: $tkcookie\n"; print "Cache-Control: private\n";

  

print <<END_of_Response;

  

<HTML> <HEAD> <TITLE>Cookie has been set</TITLE> </HEAD> <BODY> Your cookie has been set. <p>

  

END_of_Response

  

print "This is debug information you'll never see. \n"; print "Your prefs string is $prefstring <p> \n"; print "And your ID is $myID \n </body></HTML>";

  

Last but not least is our getrandomID() function, which takes the current time (in seconds) and appends a 7-digit random number to it.

sub getrandomID { return (time() . sprintf("%07d",(int(rand(5000000))))); }

  

Caution: Do not use this approach with getrandom() if you are using a version of Perl before 5.004. Perl versions before 5.004 used time() to seed the random number generator.  



TOC PREV NEXT INDEX