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.
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.
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:
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 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.
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.
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.
As hinted in the previous section, an application using Cocoon's MVC approach is composed of three layers:
- 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.
An example of such an application is the user login and preferences sample I've just checked in CVS:
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.