Last month I gave an introduction to Mason, showing how to create dynamic web pages using a few syntactic elements:
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
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.
|
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.
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).