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