Due to the inherent stateless nature of HTTP, Web technologies suffer from the problem of state information being forgotten between two successive user interactions. An interactive Web application consists of a collection of scripts wherein a single interaction comprises one script delivering a page to the browser (then ending), the user completing and submitting the form at some later point in time, and another (possibly different) script handling the submitted form. Thus, application logic is spread across a multitude of scripts.
Matters are further complicated by the fact that browsers allow users to backtrack in their interactions or clone an in-progress interaction and run both in parallel. Given this set of possibilities, a user can pursue multiple navigational paths within an application at any given time, and it's up to you to write code to ensure that each outcome is successful. Web development frameworks, such as Spring and Struts, allow you to handle multiple navigational paths, but they do so at the cost of increasing the complexity of an already overly complex code base.
In this article, I'll introduce a continuations-based alternative that can simplify the development of complex Web applications. I'll start with an introduction to continuations, including an argument for how the continuations-based approach can be a shot in the arm for the traditional MVC style of programming. Then I'll move on to a simple example: an enterprise application that demonstrates the advantages of using continuations in terms of ease of development and understanding of the application code. Because one of the chief disadvantages of using continuations is their lack of support on the Java platform, I'll use the Apache Cocoon framework to demonstrate a JavaScript implementation of the example program and a pure Java language one. I'll conclude with an overview of the pros and cons of using continuations.
Select the Code icon at the top or bottom of this article to download the example application source code. See Resources to download the Apache Cocoon framework, which you will need to run the example.
What is a continuation, anyway?
A continuation is traditionally defined as a function representing "the rest of the computation" or "what to do next." In other words, sending the intermediate result (generated by the preceding computation) to a continuation should yield the final result of the overall computation.
Consider, for example, the following rudimentary Java method that returns the square of the integer that is passed to it:
Listing 1. Method to compute square of integer input
public static int computeSquare(int x)
{
return (x*x);
}
|
This method returns a value, but leaves implicit the location to which the value should be returned. A continuation, properly applied, would make the return location explicit.
So, suppose I change the above method -- and every other method in the system -- to include an additional argument representing a continuation. Typically, this would be the last argument following all the other arguments to the method. When the function is called, it performs its internal logic as before; only, instead of returning the output value, it continues with the output value, by passing the value on to the continuation and asking the computation to resume. Thus, the above method would be rewritten as shown in Listing 2:
Listing 2. Method rewritten using a continuation object
public static int computeSquare(int x, Continuation c)
{
c.evaluate(x*x);
}
|
This programming style, in which no function is ever allowed to
return, is called Continuation Passing Style, or CPS. A function,
f1, emulates returning by passing its would-be return value to a continuation function that must have been explicitly passed to it.
Similarly, if f1 needs to call a second function,
f2, in the middle, it must pass to f2 (along with the rest of the arguments) a continuation representing the
"rest of f1." Once f2 is finished, the continuation "rest of f1" is resumed with the output of f2's computation.
Now, to add a little twist, I'll bring in another function,
f3, which is called at the tail end of f2. If f2 were to follow f1's suit in terms of passing its continuation, it would end up passing only the continuation of f1 to f3. Resuming the continuation of f1 would be all that would remain once f3 had
been executed.
Said another way, a continuation is a saved snapshot of the executable state of a program at any given point in time. It is possible to restore this state and restart the execution of the program from that point onward, such that the stack trace, all the local variables, and the program counter can reclaim their old values. See Resources to learn more about continuations. Now I'll focus on showing you what they can do to simplify your programming efforts in complex Web applications. Before we get into that, let me take a moment to further explain the problems I'm trying to address.
Problems in conventional Web development
Model-View-Controller (MVC) is the universally adopted pattern for developing interactive applications, including those for the Web. This well-known pattern organizes an interactive application into three separate modules: one for the application model with its data representation and business logic, the second for views that provide data presentation and user input, and the third for a controller to dispatch requests and control flow.
So what is this "flow" that the controller manages? A typical Web application consists of a well-defined sequence of interactions with the user in terms of loading pages and awaiting the return of filled forms. In that sense, a Web application is like an event-driven state machine. This event model is what is realized in a typical MVC architecture via the controller.
For example, suppose that the user requests a certain page from the server, which contains a form to be filled. The user spends some time thinking about and filling in the answers, then submits the form. When this event arrives at the server (the controller module), the application moves into the next logical state, depending on the present state, the data submitted by the user, and the business logic. The outcome of this state transition, as visible to the user, is that the next page in the sequence or the earlier page, along with an error message, is displayed.
This cycle is repeated as the state machine is propelled on its path from the begin state to the end state, at which point the Web application is deemed to have fulfilled the functionality required in that specific use case. The state diagram that controls the various possible flows from a "begin" state to an "end" state can either be implicitly implemented in the controller module (typically a servlet) or, as in the case of some Web development frameworks, can be externalized as metadata in configuration files.
However the framework is implemented, the basic idea of a state machine is always there. A number of issues can pop up while developing Web applications based on this model, as described below:
- Depending on the size of the state machine and the amount of data
required to maintain a client's current state (as a Web application
may be accessed by a large number of clients at any given time), the
application logic could become unnecessarily cluttered and complex.
- The client may choose to hit the back button on the browser at any
time during the sequence of state transitions, or may clone the browser
window to initiate a parallel sequence of actions. Either move could
lead to multiple (sometimes even concurrent) submissions corresponding
to states that had already been passed in the original interaction. As a
result, the application would be forced to keep track of every
individual transaction and provide the correct response to each of
them.
- A similar problem could arise in the case where a Web application was trying to gather information from the user in a series of forms spread over multiple pages. If the generation of a later form depended on the combination of responses the user had provided in the previous ones, then the application would be forced to keep track of the responses entered as part of each interaction and make sure that the correct page was returned in response to each one of them.
Model 2 Web development frameworks do, in general, provide custom techniques for mediating one or more of the above mentioned problems. However, none is as intuitive and easy to develop as the continuations-based alternative, which resolves all of these issues in one go.
About event-driven programming
The event-driven style of programming user interfaces dates back to the advent of client-server architectures. It is based around a central event processor and a number of event handlers that register with it. Each handler registers its interest in being notified when specific events occur. The user interaction state is maintained in the central module, which dispatches incoming events to the registered handlers depending on the current state held internally.
Most Web-based interactions are a special case of event-driven programming, wherein the interface display is delegated to a Web browser rather than managed by a thick-client executable running on the user's workstation. Whereas a typical thick client would disallow user-driven functionality such as backward navigation and cloning, Web browsers support, and even encourage, it. Of course, resourceful programmers have found ways to customize the browser interface (using scripting code) to disallow such operations, but this introduces a dependency on the ever changing quirks of different browsers.
While MVC's event-driven style of programming yields numerous advantages, it also leads to business functionality spread across multiple modules, making it fairly complex to develop, understand, and maintain any reasonably complex Web application. Even as numerous Web development frameworks (such as Struts, Spring, and JavaServer Faces) were developed to hide the complex plumbing beneath most MVC-style interfaces, some developers have begun to acknowledge the fact that other programming models deserve to be further explored.
A continuations-based Web application neatly sidesteps the above-mentioned problems associated with Web application development. In
contrast to MVC-based Web applications, a continuations-based application is
written as a single monolithic program. Anytime the program requires
input from the user, a Web page containing the relevant forms is sent
back to the user's browser, and a continuation representing the
remaining part of the application logic is generated and put
away. (I will soon explain your options for "putting away" the
continuation.) Because it is important to be able to restart the remaining part of the application logic upon receiving the user's response, a unique id is also generated to serve as the key to search for this particular continuation in the continuations repository. This id is also sent down, along with the page displayed to the user in an appropriate manner such that the form submission will cause the id to be sent back in the response.
When the required response arrives from the user, the server, which
is providing the continuations infrastructure for the deployed Web
application, retrieves the continuation id from the
response -- along with the regular submitted data -- and retrieves the
continuation from the repository using this id. The
continuation is then resumed (that is, the application logic starts
executing starting from the line of code immediately following the line
in which the continuation was created). Typically, the first few lines
will extract the rest of the data submitted by the user, implying that
the request object must be made available by the server within the
program, and the business logic will continue with this received data.
Upon resuming a continuation, the code that starts executing may
require more input data from the user, which will necessitate further
interaction. This is handled by creating a second continuation, saving
it in the continuations repository, and sending the form in which the
appropriate continuation id is embedded to the user. The generated outer and inner continuations can be seen to
form a tree structure with the former being a parent node and the latter
a child node. Similarly, if the business logic has conditional code
(different actions based on the outcome of an if clause) and both branches led to the formation of continuations corresponding
to possibly different data-gathering pages being sent back to the
browser, both these continuations will be the children of the
outer continuation and siblings of each other. In this way, the complete
application code can be seen to correspond to a tree of continuations -- a forest of continuations.
From the outside, the continuations-based approach cannot be
differentiated from an MVC architecture. The user is free to go back to
any previously submitted Web page, change any data required, and
resubmit the form via the browser. The difference is on the inside, in
the amount of code-juggling required to get the navigation to work
correctly. The continuations-based approach requires no additional effort
for this, because a continuation id is always
associated with each submitted page. The server just has to look up the
correct continuation for a given submitted page and ask it to resume.
Further suppose that the user is viewing a page. In terms of the so-called tree of continuations, this page is "marked" by a certain "continuation node." If the user hits the Back button once to return the page submitted previously, the marker in the continuations tree is moved up one level and set to point to the parent node of this node. This walk up the continuations tree happens each time the user hits Back. Now suppose the user stops on a certain page, re-enters data, which may or may not be the same as the data previously entered on this page, and resubmits the form. This causes the marker in the continuations tree to move down one level to point to a child node. However, because the application logic may have decided to show a different page based on the newly submitted data, the child node may actually be a sibling of the node that the marker was pointing to during the upward walk. Continuing in the same vein, the path back down the tree will lead to a different set or the same set of continuations being encountered, depending on the data submitted by the user.
While it is possible to implement continuations as a simpler
alternative to MVC frameworks, this style of programming does offer some
distinct advantages, particularly when it comes to controlling
application behavior. For example, frameworks supporting continuations
typically allow for invalidating a particular one. Invalidating a
continuation makes it impossible to go back to the page
corresponding to that continuation (by clicking on the browser's Back
button, for instance) and change the associated form data before
re-submitting the form. (Internally, the server deletes the continuation
object corresponding to the continuation id. As a result, there is no continuation available to resume, and an error is reported. This error can be handled -- in a manner specific to the development
framework being used -- by redirecting the browser to an error page, for
example. Ruling out this type of action in some instances offers a
much greater degree of control over your application's processing
overhead. As previously discussed, it is also possible to employ
scripting code in an MVC framework to disallow certain navigational
patterns. Continuations just let you do so more easily.
Unlike MVC implementations, the continuations-based approach provides a workaround to the code tangle that can result from trying to handle cloning. In the continuations-based approach, the user can enter different data on the original and the cloned windows, and submit both in parallel. The continuation is then resumed in two threads (basically, the server threads that have been assigned the handling of the two requests) with two sets of submitted values. This outcome is far preferable to what commonly happens in Web applications not based in continuations: Either such a feature is disallowed or one transaction overrides the other. Disallowing this feature is not always a good option, because users sometimes use the browser cloning feature to do what-if analyses on two sets of values prior to choosing one.
It's also worth noting that the continuations-based approach does away with the concept of user state. With continuations, a user can have multiple states at the same time, one for each cloned page open in a browser window.
It is important to maintain a continuations repository to manage the continuations for a Web application. One way is to have a global hash table with globally unique continuation ids maintained by the server providing the continuations infrastructure. This doesn't preclude one user copying and reusing the continuations id belonging to a different user from the browser. To prevent such occurrences, the tree of continuations can be maintained in a user's HTTP session, as well. In either situation, replicating the continuations repository will be necessary if running in a
clustered environment. As mentioned, invalidating a continuation will cause the supporting framework to remove the entire object from the repository. Otherwise, such frameworks also provide for specifying a time to live for continuations such that expired continuations are automatically removed from the repository.
There are two options for sending the continuation id to the user's browser: It can be embedded as a hidden field in the form that is sent back, or it can be embedded in the URL to which the form will be
posted. Needless to say, encapsulating continuation ids within cookies is a bad idea, because a specific cookie is common to all cloned instances of a browser window on a machine whereas a continuation is specific to a particular instance of the browser window only.
That's enough talk for now. The best way to make the case for continuations is to let you see them in action. In the following sections, I'll use an example application to demonstrate the simplicity of developing Web applications using continuations. To run the example application, you will need to download the Cocoon framework from Apache, because the Java platform by itself does not support continuations. See Resources to download Cocoon and learn about other Web development frameworks that support continuations.
I'll use a simple application to ease you into Web application development using continuations. From a navigational perspective, the interface for this shopping application is fairly simple. Upon accessing the first page of the application, the user is asked to enter the price and item count of the desired purchase. Upon entering this information and selecting Next, the user is taken to the next screen and asked for his category code, which determines whether he will receive a discount on the purchase amount. (Note that in this oversimplified example, it is assumed that the user will provide truthful information.) On this page, the user is asked whether the purchase should be shipped or picked up. If the shipping option is selected, the interface returns a third screen on which the user must enter the type of shipping desired: standard or express with different associated costs. If the user enters the pickup option, or upon completing the shipping option, the last screen is displayed. This screen shows, among other information, the total amount due for the purchase, which is the purchase amount, minus any given category discount, plus any shipping cost.
It is a simple application, but it will provide a good basis for learning about continuations. Before I start on the coding, I'll take a minute to introduce the Apache Cocoon framework, for those who do not already know and love it.
Web continuations in Apache Cocoon
Apache Cocoon is a Web development framework that allows you to dynamically publish XML content using XSL transformations. Cocoon's support for different transformations means that you can easily present content in multiple formats. Cocoon uses a processing pipeline to describe the sequence of steps followed in handling a request and generating the corresponding response. Each pipeline describes a way of getting some input, followed by a series of processing steps to be performed on the data, and, finally, a mechanism of producing the output.
The individual components plugged in to form the pipeline are declared and wired together in what is called a sitemap. You can define multiple pipelines for a Web application and specify that different ones be called to to handle various requests based on request/environment parameters.
The components provided by Cocoon can be classified into a number of types:
- Generators and Readers are the pipeline's input components.
- Transformers and Actions are the processing components.
- Serializers are the output components.
- Matchers and Selectors handle conditional processing.
To be useful, a pipeline must clearly contain at least a generator, or reader, and a serializer. The number of processing steps in between will depend on the business logic of the application.
The above-described Cocoon architecture corresponds to MVC's Model 1 architecture in that it lacks a central controller that dispatches requests from the client tier and selects views. However, Cocoon also has provision for Model 2 architectures. In this case, the sitemap must contain an entry that specifies the controller, in addition to the regular pipeline entries. Like in any other Model 2 architecture, the controller directs the business logic that interacts with the application model. In this case, the pipeline concept is still used for handling the view, but is driven from the controller.
The first controller engine supported by Cocoon was based on a version of Rhino JavaScript from Mozilla, because this provided support for continuations as first-class objects. As you'll see in the following example, using Cocoon with the controller means you must write the entire application as a single JavaScript program and register it as the flow controller with the sitemap specified for your Cocoon application.
All of this is much easier to understand in code than in concept. The first thing I need to do is set up the sitemap for the shopping application. I'll then go on to take a look at how the application logic is implemented in JavaScript. Lastly, I'll take a look at the XML files underlying some of the pages of the application with a view to demonstrating a few important concepts.
The sitemap for the sample shopping application is shown in Listing 3:
Listing 3. Cocoon sitemap for the sample application
<?xml version="1.0"?>
<map:sitemap xmlns:map="http://apache.org/cocoon/sitemap/1.0">
<map:flow language="javascript">
<map:script src="pos.js"/>
</map:flow>
<map:pipelines>
<map:pipeline>
<map:match pattern="page/*">
<map:generate type="jx" src="screens/{1}.xml"/>
<map:transform src="context://samples/common/style/xsl/html/simple-page2html.xsl">
<map:parameter name="servletPath" value="{request:servletPath}"/>
<map:parameter name="sitemapURI" value="{request:sitemapURI}"/>
<map:parameter name="contextPath" value="{request:contextPath}"/>
<map:parameter name="file" value="/samples/flow/jxrate/screens/{1}.xml"/>
</map:transform>
<map:serialize/>
</map:match>
</map:pipeline>
<map:pipeline>
<map:match pattern="continue.*">
<map:call continuation="{1}"/>
</map:match>
<map:match pattern="">
<map:call function="sellItem"/>
</map:match>
</map:pipeline>
</map:pipelines>
</map:sitemap>
|
The first block (flow) in the above XML file declares to Cocoon that the Flowscript interpreter must use JavaScript as the
target language and that the source for the flow logic of the
application is implemented in a file called pos.js.
The next block essentially declares the pipelines that the application will use. The three pipelines are defined as follows:
- The very first pipeline declared in the sitemap is activated by any
request to resources for which the URI matches the regular expression
page/*. The generator to be used within this pipeline, calledjx, is basically a standard generator component provided by Cocoon called theXTemplateGenerator. TheJXTemplateGeneratoris a page template processor that allows you to inject data from JavaScript objects passed by a Cocoon Flowscript (the pos.js file for our sample application) into the pipeline. The*part of the regular expression will take on a specific value, depending on the incoming request, and that value is available to the generator as{1}. So, for example, a request to the URIpage/Awill lead to theJXTemplateGeneratorusing screens/A.xml as the source. - The second pipeline is activated by any request to resources for which the URI matches the regular expression
continue.*. As you will see, the sample application passes continuationids to the browser by making them part of the URL to which the form is submitted. As before, the actual continuationidwill be available as{1}. Thecallelement essentially asks to resume the continuation identified by whatever value{1}currently holds. There is a second variation of thecallelement, which you'll see next. - The last pipeline declared in the sitemap essentially matches any
request that doesn't match any of the previous two regular expressions
(for example, the very first request made to the application will be to
the URI "
/" and, hence, will activate this pipeline). Here, thecallelement asks that the sitemap invoke the top-level functionsellItemdefined in the Flowscript (pos.js). The net effect will be that the first request to the application (URI "/") will kick off the execution of pos.js.
Note that all components must normally be declared in the sitemap. I
did not need to declare the application's components in Listing 3
because I am running my example application as part of the Cocoon
samples, which come with a top-level sitemap that declares the
components (such as jx) for me.
My next step is to write the application logic into my FlowControl
file -- in this case, pos.js. As I mentioned, the file contains a
function called sellItem that kicks off the application
flow, as shown in Listing 4:
Listing 4. Application flow implemented in JavaScript
function sellItem()
{
var rate, qty, zone, amount, discount, total, discrate, savings, delOpt, delCost, Webcon;
var url = "page/getRateAmt";
cocoon.sendPageAndWait(url);
rate = parseFloat(cocoon.request.getParameter("rate"));
qty = parseInt(cocoon.request.getParameter("qty"));
amount = rate*qty;
url="page/getZone";
Webcon = cocoon.sendPageAndWait(url, {"rate":rate, "qty":qty});
zone = cocoon.request.getParameter("zone");
discount=0.02;
if (zone=="A")
{
if (qty >= 100)
{
discount=0.1;
}
}
else if (zone=="B")
{
if (qty >= 200)
{
discount=0.2;
}
}
discrate = 100*discount;
savings = discount*amount;
delCost=0.0;
delOpt = cocoon.request.getParameter("delOpt");
if (delOpt=="S")
{
url="page/getShipOpt";
cocoon.sendPageAndWait(url);
delCost = parseInt(cocoon.request.getParameter("delCost"));
}
total = amount + delCost - savings;
url="page/displayResult";
cocoon.sendPageAndWait(url, {"discrate":discrate, "total":total, "savings":savings,
"delCost":delCost, "amount":amount, "discount":discount, "zone":zone});
}
|
The cocoon object and its functions
You will notice in this example I have used an object called
cocoon without having first declared it elsewhere. I didn't need to declare cocoon because it is part of a set of default system objects provided by Cocoon for use in Flowscripts. This
set of objects is called the Flow Object Model (FOM).
The cocoon object is probably the most important and used object in the FOM set and is also the point of entry into the FOM. It is a global variable that represents the current sitemap. It provides
two important functions called sendPage and
sendPageAndWait. Both functions pass control to the Cocoon sitemap for generating the output page.
The former function takes two arguments, one the sitemap URI of the
page to be sent back to the client and the other a context object containing data that can be extracted and used to replace
placeholders within the generated page.
The latter function, which I've used in my application logic in
Listing 4, takes the same two arguments as the former, but with a
difference. After the page has been generated and sent back to the
client, the sendPageAndWait function generates and returns a new continuation object (also part of the FOM). At this point, the Cocoon infrastructure also internally generates a unique
continuation id and stores the mapping between the two in a global structure.
The sendPageAndWait function can also be passed a function that will be automatically executed after the pipeline
processing is complete, but before the continuation is generated. This is
an important feature in cases where the pipeline processing requires
expensive or contentious resources that should not be bundled with the rest of the execution context (because the user may ask for this continuation to be resumed after a certain amount of think time, and it wouldn't make sense to hold on to these resources the entire time). However, we haven't used that version of this function in our code.
Understanding the application logic
With the above explanation, understanding the application logic
should be easy. The script first asks for a page identified by
page/getRateAmt to be sent to the user. This matches the first pipeline in the sitemap, causing the
JXTemplateGenerator component to pick up the file
screens/getRateAmt.xml and pass it on to the next component in
the pipeline. Listing 5 shows the XML file for that component, getRateAmt.xml:
Listing 5. The XML file for getRateAmt
<?xml version="1.0"?>
<page>
<title>Get Rate and Quantity of item to be purchased</title>
<content>
<form method="post" action="continue.#{$cocoon/continuation/id}">
<para>Enter Rate: <input type="text" name="rate"/></para>
<para>Enter Quantity: <input type="text" name="qty"/></para>
<input type="submit" name="submit" value="Next"/>
</form>
</content>
</page>
|
The important point to note in this file is that the action field of the form uses a JXPath expression, #{$cocoon/continuation/id}. The expression will be
automatically replaced by the continuation id generated by Cocoon when the sendPageAndWait call corresponding to its page is executed in the control script. This will cause the form to be submitted to a URL that will match the regular expression continue.*, and thereby lead to the appropriate (the second
one declared in our sitemap, actually) pipeline in the sitemap being
activated. (This pipeline, as you saw previously, consists only of a
call statement, which will resume the continuation
identified by the given id.)
Resuming the continuation essentially brings control back to the
script on the line immediately following the call to sendPageAndWait. The next two lines extract the
rate and qty parameters from the request. The next continuation is generated when the sendPageAndWait call is made for the page/getZone page, which maps to the physical file screens/getZone.xml. This call also takes a map consisting of name/value pairs, which here are basically the two parameters submitted by the user in the previous page.
To better understand how these name/value pairs work, take a look at
the XML file underlying the /getZone page, shown in Listing 6:
Listing 6. The XML file underlying the getZone page
<?xml version="1.0"?>
<page>
<title>Get Buyer Category and Delivery Option</title>
<content>
<form method="post" action="continue.#{$cocoon/continuation/id}">
<para>You are buying #{qty} items, at #{rate} apiece</para>
<para>Please specify your category:<br/>
<input type="radio" name="zone" value="A">A</input><br/>
<input type="radio" name="zone" value="B">B</input><br/>
<input type="radio" name="zone" value="C">C</input><br/>
</para>
<para>Will you be picking up the item yourself from our warehouse,
or would you like it shipped?<br/>
<input type="radio" name="delOpt" value="P">Pickup</input><br/>
<input type="radio" name="delOpt" value="S">Shipping</input><br/>
</para>
<input type="submit" name="submit" value="Next"/>
</form>
</content>
</page>
|
This page is geared to display to the user the values he entered for the rate and quantity in the previous page. You will notice that the placeholders in this XML file are #{qty} and #{rate} -- these names will be looked up in the map and automatically replaced by the corresponding values from the map when the HTML page is being assembled. As before, the action field of this
form points to the continuation id for the given page,
which represents the child continuation of the continuation generated
for the getRateAmt page.
The rest of the application flow in Listing 4 can be understood in a
similar fashion. One interesting thing to note is that the page
getShipOpt will be shown to the user only if he selected "S" for getting the purchased items shipped. If the user has
selected that option, a new continuation will be generated. If the
user has selected "P" for pickup, then the
getShipOpt page doesn't have to be shown, and, hence, no continuation will be generated.
JavaScript vs. the Java language
This brings me to the end of the continuations-based sample application developed in JavaScript. It is also possible to develop the same application with an alternative Cocoon flow interpreter that works with pure Java language, allowing you to write the entire application logic as a single Java program. I'll show you how the Java interpreter works in a moment, but first I'd like to consider the arguments for and against writing the program in Java language.
The most common argument against using JavaScript in place of the Java language is that the latter is a more widely known and used language with extensive IDE support, an extensive catalog of design patterns and so on. In support of JavaScript, it is dynamically typed and makes rapid prototyping (in the form of quicker write/update-deploy-test cycles) possible. As a language, it is already known to a large number of Java developers from its usage on the client browser side and is very easy to pick up in any case. JavaScript is an object-oriented language, and the Rhino implementation has very good integration with the Java platform. It is possible to access and reuse any Java class or object that exists in the application. Therefore, even with the core flow implemented in JavaScript, it is possible to implement the actual business logic in the Java language (with the classes being accessed from within the JavaScript flow at the appropriate places).
In short, no one option is very obviously better than the other, and it is entirely to your personal preference to decide which of the two languages you should be using to develop your continuations-based applications. And fortunately, Cocoon lets you choose either option.
Newer releases of Cocoon provide support for a pure-Java interpreter of the flow script. Listing 7 shows the source code for the pure-Java interpreter:
Listing 7. The application flow implemented in Java code
import org.apache.cocoon.components.flow.java.AbstractContinuable;
import org.apache.cocoon.components.flow.java.VarMap;
public class PosFlow extends AbstractContinuable
{
public void doSellItem()
{
double rate, amount, total, savings;
double discount, discrate;
int qty, delCost;
String zone, delOpt;
String url = "page/getRateAmt";
sendPageAndWait(url);
rate = Float.parseFloat(getRequest().getParameter("rate"));
qty = Integer.parseInt(getRequest().getParameter("qty"));
amount = rate*qty;
url="page/getZone";
sendPageAndWait(url, new VarMap().add("rate",rate).add("qty",qty));
zone = getRequest().getParameter("zone");
discount=0.02;
if (zone.equals("A"))
{
if (qty >= 100)
{
discount=0.1;
}
}
else if (zone.equals("B"))
{
if (qty >= 200)
{
discount=0.2;
}
}
discrate = 100*discount;
savings = discount*amount;
delCost=0;
delOpt = getRequest().getParameter("delOpt");
if (delOpt.equals("S"))
{
url="page/getShipOpt";
sendPageAndWait(url);
delCost = Integer.parseInt(getRequest().getParameter("delCost"));
}
total = amount + delCost - savings;
url="page/displayResult";
sendPageAndWait(url, new VarMap()
.add("discrate",discrate)
.add("total",total)
.add("savings",savings)
.add("delCost",delCost)
.add("amount", amount)
.add("discount", discount)
.add("zone", zone));
}
}
|
As Listing 7 shows, Cocoon provides an abstract class called
AbstractContinuable with implementations for the
sendPage and sendPageAndWait functions. The
PosFlow class extends this abstract class and contains the business logic in a method called doSellItem.The implementation of this method is exactly the same as the JavaScript
implementation of sellItem in Listing 4.
Sitemap for the Java implementation
You can see the sitemap for the Java-based application in Listing 8.
As you'll note, even this looks very similar to the earlier sitemap. The
only difference is that the flow language is specified as
Java and the source of the script is specified as the
PosFlow class.
Listing 8. The Cocoon sitemap for the Java-based implementation
<?xml version="1.0"?>
<map:sitemap xmlns:map="http://apache.org/cocoon/sitemap/1.0">
<map:flow language="java">
<map:script src="PosFlow"/>
</map:flow>
<map:pipelines>
<map:pipeline>
<map:match pattern="page/*">
<map:generate type="jx" src="screens/{1}.xml"/>
<map:transform src="context://samples/common/style/xsl/html/simple-page2html.xsl">
<map:parameter name="servletPath" value="{request:servletPath}"/>
<map:parameter name="sitemapURI" value="{request:sitemapURI}"/>
<map:parameter name="contextPath" value="{request:contextPath}"/>
<map:parameter name="file" value="/samples/flow/jxrate/screens/{1}.xml"/>
<map:parameter name="remove" value="{0}"/>
</map:transform>
<map:serialize/>
</map:match>
</map:pipeline>
<map:pipeline>
<map:match pattern="continue.*">
<map:call continuation="{1}"/>
</map:match>
<map:match pattern="">
<map:call function="sellItem"/>
</map:match>
</map:pipeline>
</map:pipelines>
</map:sitemap>
|
Another small difference between the JavaScript and Java
implementations is in the way the id for the current
continuation is accessed within the XML templates corresponding to the
application's HTML pages. You can see this yourself by studying the XML
template for the getRateAmt page in the Java-based
implementation, shown in Listing 9. The continuation id can be accessed by the JXPath expression
#{$continuation/id}.
Listing 9. The XML file for the getRateAmt page
<?xml version="1.0"?>
<page>
<title>Get Rate and Quantity of item to be purchased</title>
<content>
<form method="post" action="continue.#{$continuation/id}">
<para>Enter Rate: <input type="text" name="rate"/></para>
<para>Enter Quantity: <input type="text" name="qty"/></para>
<input type="submit" name="submit" value="Next"/>
</form>
</content>
</page>
|
Pros and cons of continuations
As I've shown in the preceding sections, continuations essentially provide a way to add conversational state to Web applications. The advantages of using continuations are that unusual navigation patterns can be handled easily; it is very simple to use debugging tools to run through the entire application in one shot, instead of having to place breakpoints at multiple places in a scattered code base; and it becomes very easy to understand and communicate the program structure, as well as the possible Web navigation paths in the entire application.
The biggest problem with using continuations for Web development is that not many of the languages, frameworks, and environments commonly used to develop Web applications support them. The concept of continuations and CPS itself is seen to be arcane and not intuitive. A second big hurdle is that of how and where to store continuations. We can store them on the client side, but due to the previously mentioned issue with cookies being shared across all instances of a cloned browser window, the viable option is to store the entire continuation in a serialized form in a hidden form field. This necessitates ensuring the integrity of the continuation. We can also store them on the server side, which is what I did in the sample application, but then we have to worry about issues like garbage collection and replication across cluster nodes. Lastly, there is a lack of clarity around the efficiency (performance-wise) of continuations-based Web applications.
Designing and developing complex, interactive Web-based applications is daunting in itself and made all the more harder by the multitude of whimsical navigation paths that a browser allows application users to follow. Continuations provide an elegant mechanism for developing such Web applications as a single linear program that is easy to understand and debug. In this article, I have provided a basic introduction to the theory behind continuations, as well as a practical demonstration of how the continuations support in Apache Cocoon can be leveraged to develop complex Web applications. See Resources to learn more about continuations.
| Name | Size | Download method |
|---|---|---|
| j-contin-source.zip | 7KB | HTTP |
Information about download methods
- Download Apache Cocoon from the Cocoon homepage, the original source of
Cocoon related information.
- Struts Flow
is a port of Apache Cocoon's Control Flow to Struts that allows
development of continuations-based applications using Apache
Struts.
- Malcolm Davis's "Struts,
an open source MVC implementation" (developerWorks, February 2001) is
a good overview of MVC and the Struts framework.
- Matthias Felleisen and Amr Sabry provide an introduction to
continuations in their paper "
Continuations in Programming Practice: Introduction and Survey."
- The paper "Applications of
Continuations" (PDF) by Daniel Friedman is an often-quoted paper on the
subject of continuations.
- Don't miss this extensive collection of papers on Continuations and Continuation Passing Style.
- The paper "Advanced
Control Flows for Flexible Graphical User Interfaces" by Paul Graunke
and Shriram Krishnamurthy describes a programming pattern that can be
used to provide GUI programs with browser-like capabilities of cloning
windows and bookmarking application pages (access the list of all related files).
- Paul Graunke, Robert Bruce Findler, Shriram Krishnamurthy and
Matthias Felleisen present in their paper "
Automatically Restructuring Programs for the Web" a technique for
converting interactive Web applications into scheme-based CGI programs
that utilize continuations for application flow (access the list of all related files).
- Christian Queinnec describes how a browser's navigation actions
correspond to the use of continuations in a program in the following
paper in PDF format: "
Inverting back the inversion of control or Continuations versus
page-centric programming."
- Download Rife, a Web development framework with support for continuations.
- Seaside is a framework
for developing sophisticated Web applications in Smalltalk.
- Check out the developerWorks Web development zone
for more articles on Web application design and development.
- You'll find articles about every aspect of Java programming in the developerWorksJava technology
zone.
- Browse for books on these and other technical topics.
Abhijit Belapurkar has a bachelor's degree in computer science from the Indian Institute of Technology (IIT), Delhi, India. He has been working in the areas of architectures and information security for distributed applications for almost 10 years, and has used the Java platform to build n-tier applications for more than five years. He is a senior technical architect in the J2EE space with Infosys Technologies Limited in Bangalore, India.