Programming with HTML::Mason Part 2

By: Jonathan Swartz

Last month I gave an introduction to Mason, showing how to create dynamic web pages using a few syntactic elements:

In this article we will continue our exploration with form handling, persistent sessions, and data caching. Our sample application will be a mock stock portfolio tracker. The user can enter stock symbols into their portfolio and see a table of up-to-date prices on each visit.

For the stock price access, we'll assume the presence of an API function, get_stock_price, that takes a symbol and returns its current price by way of a remote server call. Assume that get_stock_price generally completes quickly but can occasionally take longer due to network traffic, etc.

Our application consists of four components: the main page, two components to handle adding and deleting symbols, and a utility component used for external redirects.

Here's the main component, stock.html, that shows the current portfolio and offers the add/delete forms.

stock.html:
    <& header, title=>'Sample Stock Portfolio' &>
    
    % if (@symbols) {
    
    <form action="delete_symbols" method=post>
    Your portfolio:
    
    <table>
    %   foreach my $symbol (@symbols) {
    <tr>
    <td><input type=checkbox name="check_symbols" value="<% $symbol %>"></td>
    <td><% $symbol %></td>
    <td><% $price{$symbol} %></td>
    </tr>
    %   }
    </table>
    
    <input type=submit value="Delete checked symbols">
    </form>
    
    % } else {
    Your portfolio is empty.
    % }

    <form action="add_symbols" method=post>
    Enter one or more new symbols:
    <input name="symbol_string" length=20 maxsize=80>
    <input type=submit value="Add">
    </form>

    <& footer &>

    <%init>
    # Initialize session if necessary
    $session{symbols} ||= [];
    
    # Get array of symbols
    my @symbols = @{$session{symbols}};
    
    # Get the stock price for each symbol
    my %price;
    foreach my $symbol (@symbols) {
        $price{$symbol} = get_stock_price($symbol);
    }
    </%init>

The first new concept here is the <%init> block, which defines a block of Perl code to be executed as soon as the component is called.

Notice that stock.html consists of a mostly HTML section at the top and the <%init> section at the bottom. This is a traditional format for components that increases readability for non-programmers (designers, editors, etc.). By squirreling away as much code as possible in the <%init> section, one can reduce the embedded code in the HTML body to a minimum: variable substitutions, component calls, and the occasional conditional/branch.

The second new concept is session handling. The global %session hash contains persistent information about the current user. Anything placed into it is accessible when the same user comes back. This behavior is not implemented directly by Mason, but by Jeffrey Baker's Apache::Session module (available on CPAN). The Mason distribution includes a small piece of glue code to interface with Apache::Session and create %session.

The decision to not hard-code session handling into Mason has some tradeoffs. On the down side, sessions require a bit more work to configure in Mason than in "integrated" systems. On the up side, developers gain total control over how sessions work: from the storage and locking mechanism (DBI, DBM, shared memory) to the session ID communication method (cookies, URLs) to the name of the global session hash.

Handling form submissions

stock.html contains two form actions, add_symbols and delete_symbols. Both are handled by all-Perl components which perform their duties and redirect back to the main page. Here's add_symbols:

add_symbols:
    <%init>
    # Extract list of new symbols from string
    my @new_symbols = grep(/\S/,split(/\s+/,$symbol_string));
    
    # Push new symbols onto current list
    $session{symbols} = [@{$session{symbols}},@new_symbols];
    
    # Redirect back to home page
    $m->comp('redirect', url=>'/stock.html');
    </%init>
    <%args>
    $symbol_string    # String of symbols to add
    </%args>

Since our text entry box was named "symbol_string" in the form, we declare an argument $symbol_string. This becomes a lexically scoped ("my") variable containing the text that the user entered. In general, handling form submission in Mason involves simply declaring each form input element as an argument.

(For complicated or repetitive forms with many input elements, you may prefer to skip the declaration and rely on the ever-present %ARGS hash, which contains all arguments passed to the component irrespective of declarations.)

After modifying the %session hash, we call the redirect component like so:

    $m->comp('redirect', url=>/'stock.html');
$m, the Mason Request object, provides all features and methods not covered by syntactical tags. It is analogous to mod_perl's $r. Both $m and $r are automatically available as globals inside components. The $m->comp method calls another component; there are many other methods, documented in the Mason Request manual.

Next we turn to delete_symbols, which has a similar form to add_symbols:

delete_symbols:
    <%init>
    # Create a hash of all symbols to be deleted
    my %del_hash = map(($_=>1),@check_symbols);
    
    # Set new symbols list to be the old list minus deletions
    $session{symbols} = [grep(!$del_hash{$_},@{$session{symbols}})];
    
    # Redirect back to home page
    $m->comp('redirect', url=>'/stock.html');
    </%init>
    <%args>
    @check_symbols=>()   # List of checked symbols to delete
    </%args>

Since our check boxes shared the name "check_symbols" in the form, we declare an argument @check_symbols. This becomes a lexically scoped array of all symbols checked.

Finally, our redirect component:

redirect:
    <%init>
    $r->header_out(Location=>$url);
    
    # Do this so Apache doesn't try to re-read post data
    $r->method('GET');
    $r->headers_in->unset('Content-length');
    
    # Exit with 302 status
    $m->abort(302);
    </%init>
    <%args>
    $url    # URL to redirect to
    </%args>

$r->header_out is the standard mod_perl method for setting an output header. $m->abort provides an easy way to complete a request without finishing the previous components on the stack. The argument, if provided, becomes the return value from the mod_perl handler. In this case 302 is the standard Apache code for redirects.

The second and third line of <%init>, courtesy of Stas Bekman's mod_perl guide, are needed when redirecting from a POST to prevent Apache from re-reading posted data.

Data Caching

stock.html currently calls get_stock_price for every symbol in every visitor's portfolio. This is unnecessarily inefficient, especially if we decide that slightly delayed stock prices (say up to two minutes old) are accurate enough for our users.

Mason's data caching mechanism provides each component with its own persistent, expirable hash of unlimited size and data complexity. We will use this mechanism to cache each stock's price for two minutes. Here's the change to stock.html:

stock.html (2):
    .
    .
    <%init>
    # Initialize session if necessary
    $session{symbols} ||= [];
    
    # Get array of symbols
    my @symbols = @{$session{symbols}};
    
    # Get the stock price for each symbol,
    # from the cache if possible
    my %price;
    foreach my $symbol (@symbols) {
        my $value;
        unless ($value = $m->cache(key=>$symbol)) {
            $value = get_stock_price($symbol);
            $m->cache(action=>'store', key=>$symbol,
		      value=>$value, expire_in=>'2min');
        }
        $price{$symbol} = $value;
    }
    </%init>

$m->cache is used both to retrieve and store values. The first call searches the cache for a value associated with $symbol, returning the price if found and undef otherwise. In the latter case we compute the stock price and then store the price with the second call to $m->cache. The end result of our change is that all requests for a given price in a two minute period are captured by a single call to get_stock_price. The cache is shared by all users, all server processes and, if you're on a network file system, all machines.

In general, $m->cache accepts a variety of expiration times (e.g. x minutes/hours or end of day) and conditions (e.g. when a certain file or database table changes). You can also expire individual cache values or entire caches from outside applications, such as cron jobs that update a database. A convenient sister method, $m->cache_self, allows you to easily cache the HTML output of an entire component or page.

Mason caching is powerful but has room to grow. Future plans include a greater variety of storage mechanisms, a more robust object-oriented interface, and expiration based on traditional usage measurements (e.g. LRU).

Summary

In this article we've covered In the next article we'll discuss advanced site development techniques including templates, filters, embedded components, and Mason's new object-oriented features. See you then! As always, information about Mason can be found at http://www.masonhq.com/.