1use warnings; 2use strict; 3 4package Jifty::Continuation; 5 6=head1 NAME 7 8Jifty::Continuation - Allows for basic continuation-based programming 9 10=head1 DESCRIPTION 11 12In programming, a continuation is a construct that allows you to freeze the current state of a program and then recover that state later by calling the continuation. For example, you could save a continuation when throwing an exception to save the state, an exception handler could resolve the problem that caused the exception, and then call the continuation to resume execution at the point where the exception was thrown now that the problem has been solved. 13 14In Jifty, continuations are used to save the state of a request (and sometimes the response). Continuations can be used in situations such as these: 15 16=over 17 18=item 1. 19 20A user visits a page that requires login to view. The dispatcher saves a continuation and then sends the user off to the login page. Once the user logs in successfully, the login action can call the continuation to return the user back to the original page. 21 22=item 2. 23 24A blogging application might have a "Edit" link on each post to allow the editor to jump to the change page. If this link includes a saved continuation, then the "Save" button could trigger that continuation to be called to return the user back to the original page where they clicked "Edit". This way, it could return the user to the view page, or a list page, or an administrative view depending on which page the user started the edit process from. 25 26=item 3. 27 28If you have a wizard for editing some information in your application, but entering some data may require jumping to another page you can save a continuation to allow the user to return after editing. If that page also requires a jump to yet another page, you can save another continuation. Since continuations save a stack of previous continuations, you can return twice to get back to the wizard. 29 30=back 31 32C<Jifty::Continuation> handles the details of saving this information for later recovery. When a continuation is saved, the current request and response are saved to the database in the current user's session. When a continuation is called, the current request and response become those that were saved in the continuation. A continuation can be called at any point in the same session. 33 34Continuations store a L<Jifty::Request> object and the 35L<Jifty::Response> object for the request. They can also store 36arbitrary code to be run when the continuation is called. 37 38Continuations can also be arbitrarily nested. This means that 39returning from one continuation will drop you into the continuation 40that is one higher in the stack. 41 42Continuations are generally created just before their request would 43take effect, activated by the presence of certain query parameters. 44The rest of the request is saved, its execution is to be continued at a 45later time. 46 47Continuations are run after any actions have run. When a continuation 48is run, it restores the request that it has saved away into it, and 49pulls into that request the values any return values that were 50specified when it was created. The continuations code block, if any, 51is then called, and then the filled-in request is then passed to the 52L<Jifty::Dispatcher>. 53 54=cut 55 56 57use Storable 'dclone'; 58$Storable::Deparse = 1; 59$Storable::Eval = 1; 60$Storable::forgive_me = 1; 61 62use base qw/Class::Accessor::Fast Jifty::Object/; 63 64__PACKAGE__->mk_accessors(qw(id parent 65 request response code 66 version 67 )); 68 69=head2 new PARAMHASH 70 71Saves a continuation at the current state. Possible arguments in the 72C<PARAMHASH>: 73 74=over 75 76=item parent 77 78A L<Jifty::Continuation> object, or the C<id> of one. This represents 79the continuation that this continuation should return to when it is 80called. Defaults to the current continuation of the current 81L<Jifty::Request>. 82 83=item request 84 85The L<Jifty::Request> object to save away. Defaults to an empty 86L<Jifty::Request> object. 87 88=item response 89 90The L<Jifty::Response> object that will be loaded up when the 91continuation is run. Most of the time, the response isn't stored in 92the continuation, since the continuation was saved away B<before> the 93actions got run. In the case when continuations are used to preserve 94state across a redirect, however, we tuck the L<Jifty::Response> value 95of the previous request into the continuation as well. Defaults to an 96empty L<Jifty::Response> object. 97 98=item code 99 100An optional subroutine reference to evaluate when the continuation is 101called. 102 103=back 104 105=cut 106 107sub new { 108 my $class = shift; 109 my $self = bless { }, $class; 110 111 my %args = ( 112 parent => Jifty->web->request->continuation, 113 request => Jifty::Request->new(), 114 response => Jifty::Response->new(), 115 code => undef, 116 @_, 117 version => 2, 118 ); 119 120 # We don't want refs 121 $args{parent} = $args{parent}->id 122 if $args{parent} and ref $args{parent}; 123 124 # We're getting most of our properties from the arguments 125 for (keys %args) { 126 $self->$_($args{$_}) if $self->can($_); 127 } 128 129 # Generate a hopefully unique ID 130 # FIXME: use a real ID 131 my $key = Jifty->web->serial . "_" . int(rand(10)) . int(rand(10)) . int(rand(10)) . int(rand(10)) . int(rand(10)) . int(rand(10)); 132 $self->id($key); 133 134 # Make sure we don't store any of the connection information 135 my $req = $self->request; 136 local $req->{env}; 137 local $req->{_body_parser}{input_handle} if defined $req->{_body_parser}; 138 # We may also need to clean out the top request, if this is a subrequest 139 $req = $req->top_request; 140 local $req->{env}; 141 local $req->{_body_parser}{input_handle} if defined $req->{_body_parser}; 142 143 # Save it into the session 144 Jifty->web->session->set_continuation($key => $self); 145 146 return $self; 147} 148 149=head2 return_path_matches 150 151Returns true if the continuation matches the current request's path, 152and it would return to its caller in this context. This can be used 153to ask "are we about to call a continuation?" 154 155=cut 156 157sub return_path_matches { 158 my $self = shift; 159 160 return unless Jifty->web->request->path eq $self->request->path 161 or Jifty->web->request->path eq $self->request->uri->canonical->path; 162 163 my $args = Jifty->web->request->arguments; 164 return unless scalar keys %{$args} == 1; 165 166 return unless exists $args->{"J:RETURN"} and $args->{"J:RETURN"} eq $self->id; 167 168 return 1; 169} 170 171=head2 call 172 173Call the continuation; this is generally done during request 174processing, after actions have been run. 175L<Jifty::Request::Mapper>-controlled values are filled into the stored 176request based on the current request and response. During the 177process, another continuation is created, with the filled-in results 178of the current actions included, and the browser is redirected to the 179proper path, with that continuation. 180 181=cut 182 183sub call { 184 my $self = shift; 185 186 $self->log->debug("Redirect to @{[$self->request->path]} via continuation"); 187 if (Jifty->web->request->argument('_webservice_redirect')) { 188 # for continuation - perform internal redirect under webservices. 189 Jifty->web->webservices_redirect($self->request->path); 190 return; 191 } 192 193 # Clone our request 194 my $request = $self->request->clone; 195 196 # Fill in return value(s) into correct part of $request 197 $request->do_mapping; 198 199 my $response = $self->response; 200 201 # If the current response has results, we need to pull them 202 # in. For safety, monikers from the saved continuation 203 # override those from the request prior to the call 204 if (Jifty->web->response->results) { 205 $response = dclone(Jifty->web->response); 206 my %results = $self->response->results; 207 $response->result($_ => $results{$_}) for keys %results; 208 } 209 210 # Make a new continuation with that request 211 my $next = Jifty::Continuation->new(parent => $self->parent, 212 request => $request, 213 response => $response, 214 code => $self->code, 215 ); 216 $next->request->continuation(Jifty->web->session->get_continuation($next->parent)) 217 if defined $next->parent; 218 219 # Redirect to right page if we're not there already 220 # $next maybe only set path 221 my $path = 222 $next->request->request_uri 223 ? URI->new( $next->request->request_uri )->path 224 : $next->request->path; 225 Jifty->web->_redirect($path . "?J:RETURN=" . $next->id); 226 return 1; 227} 228 229=head2 return 230 231Returns from the continuation by pulling out the stored request, and 232setting that to be the active request. This shouldn't need to be 233called by hand -- use L<Jifty::Request/return_from_continuation>, 234which ensures that all requirements are met before it calls this. 235 236=cut 237 238sub return { 239 my $self = shift; 240 241 # Pull state information out of the continuation and set it 242 # up; we use clone so that the continuation itself is 243 # immutable. 244 Jifty->web->response(dclone($self->response)); 245 246 # Run any code in the continuation 247 $self->code->(Jifty->web->request) 248 if $self->code; 249 250 # We want to preserve the current actual request environment 251 # (headers, etc) 252 my $env = Jifty->web->request->top_request->env; 253 my $headers = Jifty->web->request->top_request->headers; 254 # XXX TODO: should we be saving cookies too since they're no longer pulled 255 # from env as of 1d90579c4448ceba113fae1bbdfd515c021b5ae0? 256 257 # Set the current request to the one in the continuation 258 Jifty->web->request($self->request->clone); 259 260 # Restore the environment we came in with 261 Jifty->web->request->top_request->{env} = $env; 262 Jifty->web->request->top_request->headers($headers); 263 Jifty->web->request->setup_subrequest_env 264 if Jifty->web->request->is_subrequest; 265 266 return Jifty->web->request; 267} 268 269=head2 delete 270 271Remove the continuation, and any continuations that would return to 272its scope, from the session. 273 274=cut 275 276sub delete { 277 my $self = shift; 278 279 # Remove all continuations that point to me 280 my %continuations = Jifty->web->session->continuations; 281 $_->delete for grep {$_->parent eq $self->id} values %continuations; 282 283 # Finally, remove me from the list of continuations 284 Jifty->web->session->remove_continuation($self->id); 285 286} 287 288=head1 SEE ALSO 289 290L<Jifty::Manual::Continuations> 291 292=head1 LICENSE 293 294Jifty is Copyright 2005-2010 Best Practical Solutions, LLC. 295Jifty is distributed under the same terms as Perl itself. 296 297=cut 298 2991; 300