]> git.decadent.org.uk Git - maypole.git/blobdiff - lib/Maypole.pm
Modified setup_model() to load any available custom table classes.
[maypole.git] / lib / Maypole.pm
index 3077bfddc1e4466471667a1222f3f6a35160f406..6c6ff52c0fc8ef0cad989205b9925382ccb03014 100644 (file)
@@ -6,39 +6,225 @@ use warnings;
 use Maypole::Config;
 use Maypole::Constants;
 use Maypole::Headers;
+use URI();
 
-our $VERSION = '2.10';
+our $VERSION = '2.11';
 
 # proposed privacy conventions:
 # - no leading underscore     - public to custom application code and plugins
-# - single leading underscore - private to the main Maypole stack - *not* including plugins
+# - single leading underscore - private to the main Maypole stack - *not*
+#     including plugins
 # - double leading underscore - private to the current package
 
+=head1 NAME
+
+Maypole - MVC web application framework
+
+=head1 SYNOPSIS
+
+See L<Maypole::Application>.
+
+=head1 DESCRIPTION
+
+This documents the Maypole request object. See the L<Maypole::Manual>, for a
+detailed guide to using Maypole.
+
+Maypole is a Perl web application framework similar to Java's struts. It is 
+essentially completely abstracted, and so doesn't know anything about
+how to talk to the outside world.
+
+To use it, you need to create a driver package which represents your entire
+application. This is the C<BeerDB> package used as an example in the manual.
+
+This needs to first use L<Maypole::Application> which will make your package
+inherit from the appropriate platform driver such as C<Apache::MVC> or
+C<CGI::Maypole>. Then, the driver calls C<setup>.  This sets up the model classes and
+configures your application. The default model class for Maypole uses
+L<Class::DBI> to map a database to classes, but this can be changed by altering
+configuration (B<before> calling setup.)
+
+
+=head1 DOCUMENTATION AND SUPPORT
+
+Note that some details in some of these resources may be out of date.
+
+=over 4 
+
+=item The Maypole Manual
+
+The primary documentation is the Maypole manual. This lives in the 
+C<Maypole::Manual> pod documents included with the distribution. 
+
+=item Embedded POD
+
+Individual packages within the distribution contain (more or less) detailed
+reference documentation for their API.
+
+=item Mailing lists
+
+There are two mailing lists - maypole-devel and maypole-users - see
+http://maypole.perl.org/?MailingList
+
+=item The Maypole Wiki
+
+The Maypole wiki provides a useful store of extra documentation -
+http://maypole.perl.org
+
+In particular, there's a FAQ (http://maypole.perl.org/?FAQ) and a cookbook
+(http://maypole.perl.org/?Cookbook). Again, certain information on these pages
+may be out of date.
+
+=item Web applications with Maypole
+
+A tutorial written by Simon Cozens for YAPC::EU 2005 -
+http://www.droogs.org/perl/maypole/maypole-tutorial.pdf [228KB].
+
+=item A Database-Driven Web Application in 18 Lines of Code
+
+By Paul Barry, published in Linux Journal, March 2005.
+
+http://www.linuxjournal.com/article/7937
+
+"From zero to Web-based database application in eight easy steps".
+
+Maypole won a 2005 Linux Journal Editor's Choice Award
+(http://www.linuxjournal.com/article/8293) after featuring in this article. 
+
+=item Build Web apps with Maypole
+
+By Simon Cozens, on IBM's DeveloperWorks website, May 2004.
+
+http://www-128.ibm.com/developerworks/linux/library/l-maypole/
+
+=item Rapid Web Application Deployment with Maypole
+
+By Simon Cozens, on O'Reilly's Perl website, April 2004.
+
+http://www.perl.com/pub/a/2004/04/15/maypole.html
+
+=item Authentication
+
+Some notes written by Simon Cozens. A little bit out of date, but still 
+very useful: http://www.droogs.org/perl/maypole/authentication.html
+
+=item CheatSheet
+
+There's a refcard for the Maypole (and Class::DBI) APIs on the wiki -
+http://maypole.perl.org/?CheatSheet. Probably a little out of date now - it's a
+wiki, so feel free to fix any errors!
+
+=item Plugins and add-ons
+
+There are a large and growing number of plugins and other add-on modules
+available on CPAN - http://search.cpan.org/search?query=maypole&mode=module
+
+=item del.icio.us
+
+You can find a range of useful Maypole links, particularly to several thoughtful
+blog entries, starting here: http://del.icio.us/search/?all=maypole
+
+=item CPAN ratings
+
+There are a couple of short reviews here:
+http://cpanratings.perl.org/dist/Maypole
+
+=back
+
+=head1 DEMOS
+
+A couple of demos are available, sometimes with source code and configs. 
+
+=over 4 
+
+=item http://maypole.perl.org/beerdb/
+
+The standard BeerDB example, using the TT factory templates supplied in the
+distribution.
+
+=item beerdb.riverside-cms.co.uk
+
+The standard BeerDB example, running on Mason, using the factory templates
+supplied in the L<MasonX::Maypole> distribution.
+
+=item beerfb.riverside-cms.co.uk
+
+A demo of L<Maypole::FormBuilder>. This site is running on the set of Mason 
+templates included in the L<Maypole::FormBuilder> distribution. See the 
+synopsis of L<Maypole::Plugin::FormBuilder> for an example driver
+
+=back
+
+=cut
+
 __PACKAGE__->mk_classdata($_) for qw( config init_done view_object );
+
 __PACKAGE__->mk_accessors(
     qw( params query objects model_class template_args output path
         args action template error document_encoding content_type table
-        headers_in headers_out )
+        headers_in headers_out stash session)
 );
+
 __PACKAGE__->config( Maypole::Config->new() );
+
 __PACKAGE__->init_done(0);
 
-sub debug { 0 }
+=head1 CLASS METHODS
+
+=over 4
+
+=item config
+
+Returns the L<Maypole::Config> object
+
+=item setup
+
+    My::App->setup($data_source, $user, $password, \%attr);
+
+Initialise the Maypole application and plugins and model classes - see
+L<Maypole::Manual::Plugins>.
+
+If your model is based on L<Maypole::Model::CDBI>, the C<\%attr> hashref can 
+contain options that are passed directly to L<Class::DBI::Loader>, to control 
+how the model hierarchy is constructed. 
+
+Your application should call this B<after> setting up configuration data via
+L<"config">.
+
+=cut
+
+sub setup
+{
+    my $class = shift;
+    
+    $class->setup_model(@_);   
+}
+
+=item setup_model
 
-sub setup 
+Called by C<setup>. This method builds the Maypole model hierarchy. 
+
+A likely target for over-riding, if you need to build a customised model.
+
+This method also ensures any code in custom model classes is loaded, so you
+don't need to load them in the driver.
+
+=cut
+
+sub setup_model 
 {
-    my $calling_class = shift;
+    my $class = shift;
     
-    $calling_class = ref $calling_class if ref $calling_class;
+    $class = ref $class if ref $class;
     
-    my $config = $calling_class->config;
+    my $config = $class->config;
     
     $config->model || $config->model('Maypole::Model::CDBI');
     
     $config->model->require or die sprintf 
         "Couldn't load the model class %s: %s", $config->model, $@;
     
-    $config->model->setup_database($config, $calling_class, @_);
+    # among other things, this populates $config->classes
+    $config->model->setup_database($config, $class, @_);
     
     foreach my $subclass ( @{ $config->classes } ) 
     {
@@ -46,9 +232,24 @@ sub setup
         unshift @{ $subclass . "::ISA" }, $config->model;
         $config->model->adopt($subclass)
           if $config->model->can("adopt");
+
+       # Load custom model code, if it exists - nb this must happen after the 
+       # unshift, to allow code attributes to work
+       eval "use $subclass"; 
+       die "Error loading $subclass: $@"  
+            if $@ and $@ !~ /Can\'t locate \S+ in \@INC/;
     }
 }
 
+=item init
+
+Loads the view class and instantiates the view object.
+
+You should not call this directly, but you may wish to override this to add
+application-specific initialisation - see L<Maypole::Manual::Plugins>.
+
+=cut
+
 sub init 
 {
     my $class  = shift;
@@ -60,13 +261,73 @@ sub init
       || $config->display_tables( $class->config->tables );
     $class->view_object( $class->config->view->new );
     $class->init_done(1);
+}
 
+=item new
+
+Constructs a very minimal new Maypole request object.
+
+=cut
+
+sub new
+{
+    my ($class) = @_;
+    
+    my $self = bless {
+        template_args => {},
+        config        => $class->config,
+    }, $class;
+    
+    return $self;
 }
 
+=item view_object
+
+Get/set the Maypole::View object
+
+=item debug
+
+    sub My::App::debug {1}
+
+Returns the debugging flag. Override this in your application class to
+enable/disable debugging.
+
+You can also set the C<debug> flag via L<Maypole::Application>.
+
+=cut
+
+sub debug { 0 }      
+
+=item get_template_root
+
+Implementation-specific path to template root.
+
+You should only need to define this method if you are writing a new Maypole
+backend. Otherwise, see L<Maypole::Config/"template_root">
+
+=cut
+
+sub get_template_root {'.'}
+
+=back
+
+=head1 INSTANCE METHODS
+
+=head2 Workflow
+
+=over 4
+
+=item handler
+
+This method sets up the class if it's not done yet, sets some defaults and
+leaves the dirty work to C<handler_guts>.
+
+=cut
+
 # handler() has a method attribute so that mod_perl will invoke
 # BeerDB->handler() as a method rather than a plain function
 # BeerDB::handler() and so this inherited implementation will be
-# found. See e.g. "Practical mod_perl"  by Bekman & Cholet for
+# found. See e.g. "Practical mod_perl" by Bekman & Cholet for
 # more information <http://modperlbook.org/html/ch25_01.html>
 sub handler : method 
 {
@@ -75,90 +336,108 @@ sub handler : method
     
     $class->init unless $class->init_done;
 
-    # Create the request object
-    my $r = bless {
-        template_args => {},
-        config        => $class->config
-    }, $class;
+    my $self = $class->new;
     
-    $r->headers_out(Maypole::Headers->new);
+    # initialise the request
+    $self->headers_out(Maypole::Headers->new);
+    $self->get_request($req);
+    $self->parse_location;
     
-    $r->get_request($req);
+    # hook useful for declining static requests e.g. images
+    my $status = $self->start_request_hook;
+    return $status unless $status == Maypole::Constants::OK();
     
-    $r->parse_location;
+    $self->session($self->get_session);
     
-    my $status = $r->handler_guts;
+    $status = $self->handler_guts;
     
     # moving this here causes unit test failures - need to check why
     # before committing the move
-    #$status = $r->__call_process_view unless $r->output;
+    #$status = $self->__call_process_view unless $self->output;
     
     return $status unless $status == OK;
     
-    $r->send_output;
+    # TODO: require send_output to return a status code
+    $self->send_output;
     
     return $status;
 }
 
+=item handler_guts
+
+This is the main request handling method and calls various methods to handle the
+request/response and defines the workflow within Maypole.
+
+B<Currently undocumented and liable to be refactored without warning>.
+
+=cut
+
 # The root of all evil
 sub handler_guts 
 {
-    my ($r) = @_;
+    my ($self) = @_;
     
-    $r->__load_model;
+    $self->__load_model;
 
-    my $applicable = __to_boolean( $r->is_applicable );
+    my $applicable = $self->is_model_applicable;
     
-    $r->__setup_plain_template unless $applicable;
-    
-    # We authenticate every request, needed for proper session management
+    $self->__setup_plain_template unless $applicable;
+
     my $status;
-    
-    eval { $status = $r->call_authenticate };
+
+    eval { $status = $self->call_authenticate };
     
     if ( my $error = $@ ) 
     {
-        $status = $r->call_exception($error);
+        $status = $self->call_exception($error);
         
         if ( $status != OK ) 
         {
             warn "caught authenticate error: $error";
-            return $r->debug ? $r->view_object->error($r, $error) : ERROR;
+            return $self->debug ? 
+                    $self->view_object->error($self, $error) : ERROR;
         }
     }
     
-    if ( $r->debug and $status != OK and $status != DECLINED ) 
+    if ( $self->debug and $status != OK and $status != DECLINED ) 
     {
-        $r->view_object->error( $r,
+        $self->view_object->error( $self,
             "Got unexpected status $status from calling authentication" );
     }
     
     return $status unless $status == OK;
 
     # We run additional_data for every request
-    $r->additional_data;
+    $self->additional_data;
     
     if ($applicable) 
     {
-        eval { $r->model_class->process($r) };
+        eval { $self->model_class->process($self) };
         
         if ( my $error = $@ ) 
         {
-            $status = $r->call_exception($error);
+            $status = $self->call_exception($error);
             
             if ( $status != OK ) 
             {
                 warn "caught model error: $error";
-                return $r->debug ? $r->view_object->error($r, $error) : ERROR;
+                return $self->debug ? 
+                    $self->view_object->error($self, $error) : ERROR;
             }
         }
     }
     
-    # unusual path - perhaps output has been set to an error message
-    return OK if $r->output;
+    # less frequent path - perhaps output has been set to an error message
+    return OK if $self->output;
     
     # normal path - no output has been generated yet
-    return $r->__call_process_view;
+    return $self->__call_process_view;
+}
+
+sub __load_model
+{
+    my ($self) = @_;
+    $self->model_class( $self->config->model->class_of($self, $self->table) );
 }
 
 # is_applicable() returned false, so set up a plain template. Model processing 
@@ -166,425 +445,621 @@ sub handler_guts
 # access it. 
 sub __setup_plain_template
 {
-    my ($r) = @_;
+    my ($self) = @_;
     
     # It's just a plain template
-    $r->model_class(undef);
+    $self->model_class(undef);
     
-    my $path = $r->path;
+    my $path = $self->path;
     $path =~ s{/$}{};    # De-absolutify
-    $r->path($path);
+    $self->path($path);
     
-    $r->template($r->path);
+    $self->template($self->path);
 }
 
 # The model has been processed or skipped (if is_applicable returned false), 
-# any exceptions have been handled, and there's no content in $r->output
+# any exceptions have been handled, and there's no content in $self->output
 sub __call_process_view
 {
-    my ($r) = @_;
+    my ($self) = @_;
     
     my $status;
     
-    eval { $status = $r->view_object->process($r) };
+    eval { $status = $self->view_object->process($self) };
     
     if ( my $error = $@ ) 
     {
-        $status = $r->call_exception($error);
+        $status = $self->call_exception($error);
         
         if ( $status != OK ) 
         {
-            warn "caught view error: $error" if $r->debug;
-            return $r->debug ? $r->view_object->error($r, $error) : ERROR;
+            warn "caught view error: $error" if $self->debug;
+            return $self->debug ? 
+                $self->view_object->error($self, $error) : ERROR;
         }
     }
     
     return $status;
 }
 
-sub __load_model
+=item get_request
+
+You should only need to define this method if you are writing a new
+Maypole backend. It should return something that looks like an Apache
+or CGI request object, it defaults to blank.
+
+=cut
+
+sub get_request { }
+
+=item parse_location
+
+Turns the backend request (e.g. Apache::MVC, Maypole, CGI) into a Maypole
+request. It does this by setting the C<path>, and invoking C<parse_path> and
+C<parse_args>.
+
+You should only need to define this method if you are writing a new Maypole
+backend.
+
+=cut
+
+sub parse_location 
 {
-    my ($r) = @_;
-    $r->model_class( $r->config->model->class_of($r, $r->table) );
+    die "parse_location is a virtual method. Do not use Maypole directly; " . 
+               "use Apache::MVC or similar";
 }
 
-# is_applicable() should return true or false, not OK or DECLINED, because 
-# the return value is never used as the return value from handler(). There's 
-# probably a lot of code out there supplying the return codes though, so instead 
-# of changing is_applicable() to return 0 or 1, the return value is passed through 
-# __to_boolean. I think it helps handler_guts() if we don't have multiple sets of 
-# return codes being checked for different things.
-sub is_applicable 
+=item start_request_hook
+
+This is called immediately after setting up the basic request. The default
+method simply returns C<Maypole::Constants::OK>.
+
+Any other return value causes Maypole to abort further processing of the
+request. This is useful for filtering out requests for static files, e.g.
+images, which should not be processed by Maypole or by the templating engine:
+
+    sub start_request_hook
+    {
+        my ($r) = @_;
+       
+       return Maypole::Constants::DECLINED if $r->path =~ /\.jpg$/;
+       return Maypole::Constants::OK;
+    }
+
+=cut
+
+sub start_request_hook { Maypole::Constants::OK }
+
+=item is_applicable
+
+B<This method is deprecated> as of version 2.11. If you have overridden it,
+please override C<is_model_applicable> instead, and change the return type
+from a Maypole:Constant to a true/false value.
+
+Returns a Maypole::Constant to indicate whether the request is valid.
+
+=item is_model_applicable
+
+Returns true or false to indicate whether the request is valid.
+
+The default implementation checks that C<< $r->table >> is publicly
+accessible and that the model class is configured to handle the
+C<< $r->action >>.
+
+=cut
+
+sub is_model_applicable 
 {
-    my ($r) = @_;
+    my ($self) = @_;
     
-    my $config = $r->config;
+    # cater for applications that are using obsolete version
+    if ($self->can('is_applicable')) 
+    {
+        warn "DEPRECATION WARNING: rewrite is_applicable to the interface ".
+                "of Maypole::is_model_applicable\n";
+        return $self->is_applicable == OK;
+    }
+
+    # Establish which tables should be processed by the model
+    my $config = $self->config;
     
     $config->ok_tables || $config->ok_tables( $config->display_tables );
     
     $config->ok_tables( { map { $_ => 1 } @{ $config->ok_tables } } )
-      if ref $config->ok_tables eq "ARRAY";
+        if ref $config->ok_tables eq "ARRAY";
+        
+    my $ok_tables = $config->ok_tables;
       
-    my $table = $r->table;
+    # Does this request concern a table to be processed by the model?
+    my $table = $self->table;
+    
+    my $ok = 0;
     
-    warn "We don't have that table ($table).\n"
-      . "Available tables are: "
-      . join( ",", @{ $config->display_tables } )
-            if $r->debug
-                and not $config->ok_tables->{$table}
-                        and $r->action; # I think this is always true
-                        
-    return DECLINED unless exists $config->ok_tables->{$table};
+    if (exists $ok_tables->{$table}) 
+    {
+        $ok = 1;
+    } 
 
-    # Is it public?
-    return DECLINED unless $r->model_class->is_public( $r->action );
+    if (not $ok) 
+    {
+        warn "We don't have that table ($table).\n"
+            . "Available tables are: "
+            . join( ",", keys %$ok_tables )
+                if $self->debug and not $ok_tables->{$table};
+                
+        return 0;
+    }
+    
+    # Is the action public?
+    my $action = $self->action;
+    return 1 if $self->model_class->is_public($action);
+    
+    warn "The action '$action' is not applicable to the table $table"
+        if $self->debug;
     
-    return OK;
+    return 0;
 }
 
-# *only* intended for translating the return code from is_applicable()
-sub __to_boolean { $_[0] == OK ? 1 : 0 }
+=item get_session
 
+The default method is empty. 
+
+=cut
+
+sub get_session { }
+
+=item call_authenticate
+
+This method first checks if the relevant model class
+can authenticate the user, or falls back to the default
+authenticate method of your Maypole application.
 
+=cut
 
 sub call_authenticate 
 {
-    my ($r) = @_;
+    my ($self) = @_;
 
-    # Check if we have a model class
-    return $r->model_class->authenticate($r) 
-        if  $r->model_class 
-        and $r->model_class->can('authenticate');
+    # Check if we have a model class with an authenticate() to delegate to
+    return $self->model_class->authenticate($self) 
+        if $self->model_class and $self->model_class->can('authenticate');
     
-    # passing $r is unnecessary and redundant, but there's probably 
-    # a lot of code out there now using the 2nd instead of the 1st $r,
-    # so we may as well leave it
-    return $r->authenticate($r);   
+    # Interface consistency is a Good Thing - 
+    # the invocant and the argument may one day be different things 
+    # (i.e. controller and request), like they are when authenticate() 
+    # is called on a model class (i.e. model and request)
+    return $self->authenticate($self);   
 }
 
+=item authenticate
+
+Returns a Maypole::Constant to indicate whether the user is authenticated for
+the Maypole request.
+
+The default implementation returns C<OK>
+
+=cut
+
+sub authenticate { return OK }
+
+
+=item call_exception
+
+This model is called to catch exceptions, first after authenticate, then after
+processing the model class, and finally to check for exceptions from the view
+class.
+
+This method first checks if the relevant model class
+can handle exceptions the user, or falls back to the default
+exception method of your Maypole application.
+
+=cut
+
 sub call_exception 
 {
-    my ($r, $error) = @_;
+    my ($self, $error) = @_;
 
-    # Check if we have a model class
-    if ( $r->model_class && $r->model_class->can('exception') )
+    # Check if we have a model class with an exception() to delegate to
+    if ( $self->model_class && $self->model_class->can('exception') )
     {
-        my $status = $r->model_class->exception( $r, $error );
+        my $status = $self->model_class->exception( $self, $error );
         return $status if $status == OK;
     }
     
-    return $r->exception($error);
+    return $self->exception($error);
 }
 
-sub additional_data { }
+=item exception
 
-sub authenticate { return OK }
+This method is called if any exceptions are raised during the authentication or
+model/view processing. It should accept the exception as a parameter and return
+a Maypole::Constant to indicate whether the request should continue to be
+processed.
+
+=cut
 
 sub exception { return ERROR }
 
+=item additional_data
+
+Called before the model processes the request, this method gives you a chance to
+do some processing for each request, for example, manipulating C<template_args>.
+
+=cut
+
+sub additional_data { }
+
+=item send_output
+
+Sends the output and additional headers to the user.
+
+=cut
+
+sub send_output {
+    die "send_output is a virtual method. Do not use Maypole directly; use Apache::MVC or similar";
+}
+
+
+
+
+=back
+
+=head2 Path processing and manipulation
+
+=over 4
+
+=item path
+
+Returns the request path
+
+=item parse_path
+
+Parses the request path and sets the C<args>, C<action> and C<table>
+properties. Calls C<preprocess_path> before parsing path and setting properties.
+
+=cut
+
 sub parse_path 
 {
-    my ($r) = @_;
-    
-    $r->path || $r->path('frontpage');
+    my ($self) = @_;
     
-    my @pi = grep {length} split '/', $r->path;
+    # Previous versions unconditionally set table, action and args to whatever 
+    # was in @pi (or else to defaults, if @pi is empty).
+    # Adding preprocess_path(), and then setting table, action and args 
+    # conditionally, broke lots of tests, hence this:
+    $self->$_(undef) for qw/action table args/;
     
-    $r->table(shift @pi);
+    $self->preprocess_path;
+
+    $self->path || $self->path('frontpage');
     
-    $r->action( shift @pi or 'index' );
+    my @pi = grep {length} split '/', $self->path;
     
-    $r->args(\@pi);
+    $self->table  || $self->table(shift @pi);
+    $self->action || $self->action( shift @pi or 'index' );
+    $self->args   || $self->args(\@pi);
 }
 
-# like CGI::param(), but read only 
-sub param 
-{ 
-    my ($r, $key) = @_;
+=item preprocess_path
+
+Sometimes when you don't want to rewrite or over-ride parse_path but
+want to rewrite urls or extract data from them before it is parsed.
+
+This method is called after parse_location has populated the request
+information and before parse_path has populated the model and action
+information, and is passed the request object.
+
+You can set action, args or table in this method and parse_path will
+then leave those values in place or populate them if not present
+
+=cut
+
+sub preprocess_path { };
+
+=item make_path( %args or \%args or @args )
+
+This is the counterpart to C<parse_path>. It generates a path to use
+in links, form actions etc. To implement your own path scheme, just override
+this method and C<parse_path>.
+
+    %args = ( table      => $table,
+              action     => $action,        
+              additional => $additional,    # optional - generally an object ID
+              );
+              
+    \%args = as above, but a ref
     
-    return keys %{$r->params} unless defined $key;
+    @args = ( $table, $action, $additional );   # $additional is optional
+
+C<id> can be used as an alternative key to C<additional>.
+
+C<$additional> can be a string, an arrayref, or a hashref. An arrayref is
+expanded into extra path elements, whereas a hashref is translated into a query
+string. 
+
+=cut
+
+sub make_path
+{
+    my $r = shift;
     
-    return unless exists $r->params->{$key};
+    my %args;
     
-    my $val = $r->params->{$key};
+    if (@_ == 1 and ref $_[0] and ref $_[0] eq 'HASH')
+    {
+        %args = %{$_[0]};
+    }
+    elsif ( @_ > 1 and @_ < 4 )
+    {
+        $args{table}      = shift;
+        $args{action}     = shift;
+        $args{additional} = shift;
+    }
+    else
+    {
+        %args = @_;
+    }
     
-    return ref $val ? @$val : ($val) if wantarray;
-        
-    return ref $val ? $val->[0] : $val;
-}
+    do { die "no $_" unless $args{$_} } for qw( table action );    
 
-sub get_template_root {'.'}
-sub get_request       { }
-
-sub parse_location {
-    die "Do not use Maypole directly; use Apache::MVC or similar";
-}
-
-sub send_output {
-    die "Do not use Maypole directly; use Apache::MVC or similar";
+    my $additional = $args{additional} || $args{id};
+    
+    my @add = ();
+    
+    if ($additional)
+    {
+        # if $additional is a href, make_uri() will transform it into a query
+        @add = (ref $additional eq 'ARRAY') ? @$additional : ($additional);
+    }    
+    
+    my $uri = $r->make_uri($args{table}, $args{action}, @add);
+    
+    return $uri->as_string;
 }
 
-# Session and Repeat Submission Handling
 
-sub make_random_id {
-    use Maypole::Session;
-    return Maypole::Session::generate_unique_id();
-}
 
-=head1 NAME
+=item make_uri( @segments )
 
-Maypole - MVC web application framework
+Make a L<URI> object given table, action etc. Automatically adds
+the C<uri_base>. 
 
-=head1 SYNOPSIS
+If the final element in C<@segments> is a hash ref, C<make_uri> will render it
+as a query string.
 
-See L<Maypole::Application>.
+=cut
 
-=head1 DESCRIPTION
+sub make_uri
+{
+    my ($r, @segments) = @_;
 
-This documents the Maypole request object. See the L<Maypole::Manual>, for a
-detailed guide to using Maypole.
+    my $query = (ref $segments[-1] eq 'HASH') ? pop(@segments) : undef;
+    
+    my $base = $r->config->uri_base; 
+    $base =~ s|/$||;
+    
+    my $uri = URI->new($base);
+    $uri->path_segments($uri->path_segments, grep {length} @segments);
+    
+    my $abs_uri = $uri->abs('/');
+    $abs_uri->query_form($query) if $query;
+    return $abs_uri;
+}
 
-Maypole is a Perl web application framework similar to Java's struts. It is 
-essentially completely abstracted, and so doesn't know anything about
-how to talk to the outside world.
+=item parse_args
 
-To use it, you need to create a package which represents your entire
-application. In our example above, this is the C<BeerDB> package.
+Turns post data and query string paramaters into a hash of C<params>.
 
-This needs to first use L<Maypole::Application> which will make your package
-inherit from the appropriate platform driver such as C<Apache::MVC> or
-C<CGI::Maypole>, and then call setup.  This sets up the model classes and
-configures your application. The default model class for Maypole uses
-L<Class::DBI> to map a database to classes, but this can be changed by altering
-configuration. (B<Before> calling setup.)
+You should only need to define this method if you are writing a new Maypole
+backend.
 
-=head2 CLASS METHODS
+=back
 
-=head3 config
+=head2 Request properties
 
-Returns the L<Maypole::Config> object
+=over 4
 
-=head3 setup
+=item model_class
 
-    My::App->setup($data_source, $user, $password, \%attr);
+Returns the perl package name that will serve as the model for the
+request. It corresponds to the request C<table> attribute.
 
-Initialise the maypole application and model classes. Your application should
-call this after setting configuration via L<"config">
 
-=head3 init
+=item objects
 
-You should not call this directly, but you may wish to override this to
-add
-application-specific initialisation.
+Get/set a list of model objects. The objects will be accessible in the view
+templates.
 
-=head3 view_object
+If the first item in C<$self-E<gt>args> can be C<retrieve()>d by the model
+class, it will be removed from C<args> and the retrieved object will be added to
+the C<objects> list. See L<Maypole::Model> for more information.
 
-Get/set the Maypole::View object
+=item template_args
 
-=head3 debug
+    $self->template_args->{foo} = 'bar';
 
-    sub My::App::debug {1}
+Get/set a hash of template variables.
 
-Returns the debugging flag. Override this in your application class to
-enable/disable debugging.
+=item stash
 
-=head2 INSTANCE METHODS
+A place to put custom application data. Not used by Maypole itself. 
 
-=head3 parse_location
+=item template
 
-Turns the backend request (e.g. Apache::MVC, Maypole, CGI) into a
-Maypole
-request. It does this by setting the C<path>, and invoking C<parse_path>
-and
-C<parse_args>.
+Get/set the template to be used by the view. By default, it returns
+C<$self-E<gt>action>
 
-You should only need to define this method if you are writing a new
-Maypole
-backend.
 
-=head3 path
+=item error
 
-Returns the request path
+Get/set a request error
 
-=head3 parse_path
+=item output
 
-Parses the request path and sets the C<args>, C<action> and C<table> 
-properties
+Get/set the response output. This is usually populated by the view class. You
+can skip view processing by setting the C<output>.
 
-=head3 table
+=item table
 
 The table part of the Maypole request path
 
-=head3 action
+=item action
 
 The action part of the Maypole request path
 
-=head3 args
+=item args
 
 A list of remaining parts of the request path after table and action
 have been
 removed
 
-=head3 headers_in
+=item headers_in
 
 A L<Maypole::Headers> object containing HTTP headers for the request
 
-=head3 headers_out
+=item headers_out
 
 A L<HTTP::Headers> object that contains HTTP headers for the output
 
-=head3 parse_args
-
-Turns post data and query string paramaters into a hash of C<params>.
-
-You should only need to define this method if you are writing a new
-Maypole
-backend.
-
-=head3 param
-
-An accessor for request parameters. It behaves similarly to CGI::param() for
-accessing CGI parameters.
-
-=head3 params
-
-Returns a hash of request parameters. The source of the parameters may vary
-depending on the Maypole backend, but they are usually populated from request
-query string and POST data.
+=item document_encoding
 
-B<Note:> Where muliple values of a parameter were supplied, the
-C<params> 
-value
-will be an array reference.
+Get/set the output encoding. Default: utf-8.
 
-=head3 get_template_root
+=item content_type
 
-Implementation-specific path to template root.
+Get/set the output content type. Default: text/html
 
-You should only need to define this method if you are writing a new
-Maypole
-backend. Otherwise, see L<Maypole::Config/"template_root">
+=item get_protocol
 
-=head3 get_request
+Returns the protocol the request was made with, i.e. https
 
-You should only need to define this method if you are writing a new
-Maypole backend. It should return something that looks like an Apache
-or CGI request object, it defaults to blank.
+=cut
 
+sub get_protocol {
+  die "get_protocol is a virtual method. Do not use Maypole directly; use Apache::MVC or similar";
+}
 
-=head3 is_applicable
+=back
 
-Returns a Maypole::Constant to indicate whether the request is valid.
+=head2 Request parameters
 
-The default implementation checks that C<$r-E<gt>table> is publicly
-accessible
-and that the model class is configured to handle the C<$r-E<gt>action>
+The source of the parameters may vary depending on the Maypole backend, but they
+are usually populated from request query string and POST data.
 
-=head3 authenticate
+Maypole supplies several approaches for accessing the request parameters. Note
+that the current implementation (via a hashref) of C<query> and C<params> is
+likely to change in a future version of Maypole. So avoid direct access to these
+hashrefs:
 
-Returns a Maypole::Constant to indicate whether the user is
-authenticated for
-the Maypole request.
+    $r->{params}->{foo}      # bad
+    $r->params->{foo}        # better
 
-The default implementation returns C<OK>
+    $r->{query}->{foo}       # bad
+    $r->query->{foo}         # better
 
-=head3 model_class
+    $r->param('foo')         # best
 
-Returns the perl package name that will serve as the model for the
-request. It corresponds to the request C<table> attribute.
+=over 4
 
-=head3 additional_data
+=item param
 
-Called before the model processes the request, this method gives you a
-chance
-to do some processing for each request, for example, manipulating
-C<template_args>.
+An accessor (get or set) for request parameters. It behaves similarly to
+CGI::param() for accessing CGI parameters, i.e.
 
-=head3 objects
+    $r->param                   # returns list of keys
+    $r->param($key)             # returns value for $key
+    $r->param($key => $value)   # returns old value, sets to new value
 
-Get/set a list of model objects. The objects will be accessible in the
-view
-templates.
+=cut
 
-If the first item in C<$r-E<gt>args> can be C<retrieve()>d by the model
-class,
-it will be removed from C<args> and the retrieved object will be added
-to the
-C<objects> list. See L<Maypole::Model> for more information.
+sub param 
+{ 
+    my ($self, $key) = (shift, shift);
+    
+    return keys %{$self->params} unless defined $key;
+    
+    return unless exists $self->params->{$key};
+    
+    my $val = $self->params->{$key};
+    
+    if (@_)
+    {
+        my $new_val = shift;
+       $self->params->{$key} = $new_val;
+    }
+    
+    return ref $val ? @$val : ($val) if wantarray;
+        
+    return ref $val ? $val->[0] : $val;
+}
 
-=head3 template_args
 
-    $r->template_args->{foo} = 'bar';
+=item params
 
-Get/set a hash of template variables.
+Returns a hashref of request parameters. 
 
-=head3 template
+B<Note:> Where muliple values of a parameter were supplied, the C<params> value
+will be an array reference.
 
-Get/set the template to be used by the view. By default, it returns
-C<$r-E<gt>action>
+=item query
 
-=head3 exception
+Alias for C<params>.
 
-This method is called if any exceptions are raised during the
-authentication 
-or
-model/view processing. It should accept the exception as a parameter and 
-return
-a Maypole::Constant to indicate whether the request should continue to
-be
-processed.
+=back
 
-=head3 error
+=head3 Utility methods
 
-Get/set a request error
+=over 4
 
-=head3 output
+=item redirect_request
 
-Get/set the response output. This is usually populated by the view
-class. You
-can skip view processing by setting the C<output>.
+Sets output headers to redirect based on the arguments provided
 
-=head3 document_encoding
+Accepts either a single argument of the full url to redirect to, or a hash of
+named parameters :
 
-Get/set the output encoding. Default: utf-8.
+$r->redirect_request('http://www.example.com/path');
 
-=head3 content_type
+or
 
-Get/set the output content type. Default: text/html
+$r->redirect_request(protocol=>'https', domain=>'www.example.com', path=>'/path/file?arguments', status=>'302', url=>'..');
 
-=head3 send_output
+The named parameters are protocol, domain, path, status and url
 
-Sends the output and additional headers to the user.
+Only 1 named parameter is required but other than url, they can be combined as
+required and current values (from the request) will be used in place of any
+missing arguments. The url argument must be a full url including protocol and
+can only be combined with status.
 
-=head3 call_authenticate
+=cut
 
-This method first checks if the relevant model class
-can authenticate the user, or falls back to the default
-authenticate method of your Maypole application.
+sub redirect_request {
+  die "redirect_request is a virtual method. Do not use Maypole directly; use Apache::MVC or similar";
+}
 
+=item redirect_internal_request 
 
-=head3 call_exception
+=cut
 
-This model is called to catch exceptions, first after authenticate, then after
-processing the model class, and finally to check for exceptions from the view
-class.
+sub redirect_internal_request {
 
-This method first checks if the relevant model class
-can handle exceptions the user, or falls back to the default
-exception method of your Maypole application.
+}
 
-=head3 make_random_id
 
-returns a unique id for this request can be used to prevent or detect repeat submissions.
+=item make_random_id
 
-=head3 handler
+returns a unique id for this request can be used to prevent or detect repeat
+submissions.
 
-This method sets up the class if it's not done yet, sets some
-defaults and leaves the dirty work to handler_guts.
+=cut
 
-=head3 handler_guts
+# Session and Repeat Submission Handling
+sub make_random_id {
+    use Maypole::Session;
+    return Maypole::Session::generate_unique_id();
+}
 
-This is the core of maypole. You don't want to know.
+=back
 
 =head1 SEE ALSO
 
@@ -597,7 +1072,7 @@ L<Maypole::Application>, L<Apache::MVC>, L<CGI::Maypole>.
 
 =head1 AUTHOR
 
-Maypole is currently maintained by Simon Flack C<simonflk#cpan.org>
+Maypole is currently maintained by Aaron Trevena
 
 =head1 AUTHOR EMERITUS