September 09, 2002

Model-View-Controller in Cocoon using continuations-based control flow

Cocoon


I've finally had time to finalize some changes to the control flow layer of Cocoon, which should make it more usable. I've also wrote a simple application that makes use of these changes and shows how the control flow layer is supposed to be used. I also plan to write some real documentation on how to use the control flow layer. Before then however, I'll write here some quick thoughts on how this works. At some point I'll take these and transform them into a real document.



Model-View-Controller

With the control flow architecture in place a Cocoon Web application is split in three different conceptual layers, using the well-known Model-View-Controller (MVC) pattern:

  • Model: the business logic, e.g. Java classes which implements the meat of your application.
  • View: the collection of the XML pages and XSLT stylesheets that give a visual representation to your business objects. In Cocoon the pages used in the View layer can be simple XML files or XSP pages that use the JXPath logicsheet. More on this later.
  • Controller, which is coordinating the sequence of pages being sent to the client browser, based on the actions taken by the user. In Cocoon, the Controller is a specialized engine which uses a modified Rhino JavaScript implementation which supports continuations as first class objects. From a user (really a developer) perspective, the Controller is nothing else than a collection of JavaScript files running on the server side.

The general flow of actions in an application which uses the control flow is as described below.

The request is received by Cocoon and passed to the sitemap for processing. In the sitemap, you can do two things to pass the control to the Controller layer:

  • you can invoke a JavaScript top-level function to start processing a logically grouped sequences of pages. Each time a response page is being sent back to the client browser from this function, the processing of the JavaScript code stops at the point the page is sent back, and the HTTP request finishes. Through the magic of continuations, the execution state is saved in a continuation object. Each continuation is given a unique string id, which could be embedded in generated page, so that you can restart the saved computation later on.

    To invoke a top level JavaScript function in the Controller, you use the <map:call function="function-name"/> construction.

  • to restart the computation of a previously stopped function, you use the <map:continue with="..."/> construction. This restarts the computation saved in a continuation object identified by the string value of the with attribute. This value could be extracted in the sitemap from the requested URL, from a POST or GET parameter etc. When the computation stored in the continuation object is restarted, it appears as if nothing happened, all the local and global variables have exactly the same values as they had when the computation was stopped.

Once the JavaScript function in the control layer is restarted, you're effectively inside the Controller. Here you have access to the request parameters, and to the business logic objects. The controller script takes the appropriate actions to invoke the business logic, usually written in Java, creating objects, setting various values on them etc.

When the business logic is invoked, you're inside the Model. The business logic takes whatever actions are needed, accessing a database, making a SOAP request to a Web service etc. When this logic finishes, the program control goes back to the Controller.

Once here, the Controller has to decide which page needs to be sent back to the client browser. To do this, the script can invoke either the sendPage or the sendPageAndContinue functions. These functions take two parameters, the relative URL of the page to be sent back to the client, and a context object which can be accessed inside this page to extract various values and place them in the generated page.

The second argument to sendPage and sendPageAndContinue is a context object, which can be a simple dictionary with values that need to be displayed by the View. More generally any Java or JavaScript object can be passed here, as long as the necessary get methods for the important values are provided.

The page specified by the URL is processed by the sitemap, using the normal sitemap rules. The simplest case is an XSP generator followed by an XSLT transformation and a serializer. This page generation is part of the View layer. If an XSP page is processed, you can make use of JXPath elements to retrieve values from the context objects passed by the Controller.

The JXPath elements mirror similar XSLT constructions, except that instead of operating on an XML document, operate on a Java or JavaScript object. The JXPath logicsheet has constructs like jpath:if, jpath:choose, jpath:when, jpath:otherwise, jpath:value-of and jpath:for-each, which know how to operate on hierarchies of nested Java objects. Historically the namespace is called jpath instead of jxpath, we'll probably change it to the latter before the next major release.

A special instruction, jpath:continuation returns the id of the continuation that restarts the processing from the last point. It can actually retrieve ids of earlier continuations, which represent previous stopped points, but I'm not discussing about this here to keep things simple.

Going back to the sendPage and sendPageAndContinue functions, there is a big difference between them. The first function will send the response back to the client browser, and will stop the processing of the JavaScript script by saving it into a continuation object. The other function, sendPageAndContinue will send the response, but it will not stop the computation. This is useful for example when you need to exit a top-level JavaScript function invoked with <map:call function="..."/>.

The above explains how MVC could be really achieved in Cocoon with the control flow layer. Note that there is no direct communication between Model and View, everything is directed by the Controller by passing to View a context object constructed from Model data. In a perfect world, XSP should have only one logicsheet, the JXPath logicsheet. There should be no other things in an XSP page that put logic in the page (read View), instead of the Model. If you don't like XSP, and prefer to use JSP or Velocity, the JXPath logicsheet equivalents should be implemented.

Basic usage

As hinted in the previous section, an application using Cocoon's MVC approach is composed of three layers:

  • a JavaScript controller which implements the interaction with the client

  • the business logic model which implements your application

  • the XSP pages, which describe the content of the pages, and XSLT stylesheets which describe the look of the content.

In more complex applications, the flow of pages can be thought of smaller sequences of pages which are composed together. The natural analogy is to describe these sequences in separate JavaScript functions, which can then be called either from the sitemap, can call each other freely.

An example of such an application is the user login and preferences sample I've just checked in CVS:

http://cvs.apache.org/viewcvs.cgi/xml-cocoon2/src/webapp/samples/flow/examples/prefs

This application is composed of four top-level JavaScript functions: login, registerUser, edit and logout.

The entry level point in the application can be any of these functions, but in order for a user to use the application, (s)he must login first. Once the user logs in, we want to maintain the Java User object which represents the user between top-level function invocations.

If the script does nothing, each invocation of a top-level function starts with fresh values for the global variables, no global state is preserved between top-level function invocations from the sitemap. In this sample for example, the login function assigns to the global variable user the Java User object representing the logged in user. The edit function trying to operate on this object would get a null value instead, because the value is not shared by default between these top-level function invocations.

To solve the problem, the login and registerUser functions have to call the cocoon.createSession() method, which creates a servlet session and saves the global scope containing the global variables' value in it. Next time the user invokes one of the four top-level functions, the values of the global variables is restored, making sharing very easy.

Even if you don't need complex control flow in your application, you may still choose to use the MVC pattern described above. You can have top-level JavaScript functions which obtain the request parameters, invoke the business logic and then call sendPageAndContinue to generate a response page and return from the computation. Since there's no continuation object being created by this function, and no global scope being saved, there's no memory resource being eaten. The approach provides a clean way of separating logic and content, and makes things easy to follow, since you have to look at a single script to understand what's going on.

Posted by ovidiu at September 09, 2002 03:03 AM |
 
Copyright © 2002-2011 Ovidiu Predescu.