Brian's Waste of Time

Wed, 26 Nov 2003

Apache Cocoon FlowScript and Woody Observations

After Ovidiu Predescu's Cocoon FlowScript presentation at ApacheCon I determined that I needed to give Cocoon another look. Previously I had been impressed by it, but not impressed enough to actually continue using it. We have been wanting to redo a number of small intranet applications here for such simple things as setting vacation messages, updating login information, user management against our ldap, etc. The natural way to do this is via a webapp, so here was a chance tyo spike it against Cocoon and FlowScript. This article looks at some things I learned in the last two days spiking this app. It uses Cocoon with FlowScript, Woody, and JXTemplate Generator.

Logging in to this mysterious application was the first requirement and fell into place pretty easily. I ran into a fair bit of weirdness trying to populate data into the HttpSession. I swapped some emails with Reinhard Poetz on the cocoon-users list and the first ah-hah occured. He pointed out that I shouldn't be trying to put things into the HttpSession, but just keep all of "session" information as globals (ick, but it works so far) in the FlowScript. Pass information into the JXTemplates via the showPage("page", {"user": user, "message": message}) map passed into this. I am afraid it will be a problem down the road at there is a lot of duplication building a standard map structure, but I'll generalize it when I need to.

So, the final login FlowScript looks like:


cocoon.load("resource://org/apache/cocoon/woody/flow/javascript/woody2.js");

var user = null;
var security = new Packages.com.forthillcompany.infra.serv.SecurityService();

/**
 * Login and populate global user
 */
function login()
{
    var form = new Form("forms/login.xml");
    var message = "";
    while (user == null)
    {
        form.showForm("login", {"message": message});
        user = security.login(form.getModel().name, form.getModel().password);
        if (user == null)
        {
            message = "login.error.invalidLogin";
        }
    }
    cocoon.sendPage("home", {"user": user});
}

/**
 * Reset the current user
 */
function logout()
{
    user = null;
    cocoon.sendPage("login.exec");
}
The Form stuff is Woody, more of which I'll discuss in a bit -- but not in much detail for this article. The observation here is that the user variable is a global within the FlowScript making it available to any other FlowScript script later (such as the vacation message control stuff).

The second piece of FlowScript I did was simply a password updater. This one is a bit uglier and I am far from happy with it, but it works.


cocoon.load("resource://org/apache/cocoon/woody/flow/javascript/woody2.js");

/**
 * Change an Employee's password
 */
function changePassword()
{
    var form = new Form("forms/changePassword.xml");
    var fail = true;
    var message = "";
    while (fail)
    {
        form.showForm("ChangePassword", {"user": user, "message": message});
        var model = form.getModel();
        if (model.newPassword == model.newPasswordAgain)
        {
            if (security.changePassword(user.uid, model.oldPassword, model.newPassword))
            {
                // Password has been changed successfully
                fail = false;
            }
            else
            {
                message = "ChangePassword.error.oldPasswordIncorrect";
            }
        }
        else
        {
            message = "ChangePassword.error.passwordsDoNotMatch";
        }
    }
    cocoon.sendPage("home", {"user": user});
}
Notice that this uses the user object representing the logged in user, and the security object providing access to security related services defined in the login/logout script. These are seperate scripts but the globals are just that -- global.

FlowScript lends itself nicely to Service oriented API's rather than Command oriented API's so common in web applications. These are extremely simple things, logging in, and changing a series of passwords (changePassword updates the samba stuff too) -- but even in a complex domain I think that services are easier to work with and maintain than encapsulated atomic commands, usually.

A side effect of the "globals as session" idiom is that every call needs to go into a FlowScript script (the controller in wince web-mvc talk) instead of going directly to a page, even if the script doesn;t do anything but display a page. I found this inconvienent at first, but a showPage script that took a parameter with the pipeline name would be easy to toss together, and is probably what I will do when I need it.

The requirement to enter every page through a script hurt at first until I remembered how flexible the sitemap pipeline matchers are, I think I will need just one more matcher, the aforementioned showPage script caller as it will need to extract a pipeline name from the uri, in addition to the following:


    <map:pipelines>
        <map:pipeline>
            <!-- default to login -->
            <map:match pattern="">
                <map:call function="login"/>
            </map:match>

            <!-- enter a new FlowScript -->
            <map:match pattern="*.exec">
                <map:call function="{1}"/>
            </map:match>

            <!-- match continuations -->
            <map:match pattern="*.continue">
                <map:call continuation="{1}"/>
            </map:match>

            <map:match pattern="*">
                <map:generate type="jx" src="{1}.jxt"/>
                <map:transform type="woody"/>
                <map:transform type="i18n">
                    <map:parameter name="locale" value="en-US"/>
                </map:transform>
                <map:transform src="resources/woody-fields-fhc.xsl"/>
                <!-- <map:transform src="resources/html-tidy.xsl"/> -->
                <map:serialize type="xhtml"/>
            </map:match>
        </map:pipeline>
    </map:pipelines>
Anything ending in .exec will enter a new FlowScript, .continue will specify the continuation, and the final one is only used from within FlowScript to display pages. I will probably wind up obfuscating it some as no one should ever see one of those in the adress bar of their browser and it is an obvious guess.

The great mysterious form handling in the FlowScript is handled by Woody. I am not nearly as impressed by Woody as I am by FlowScript, but it works pretty well. It has a binding ability to bind forms to objects which I haven't played with, but the way that you need to specify forms in your templates ad go through multiple transformations to make them useful is annoying, and breaks one of the big requirements I have for most webapps - play nicely with Dreamweaver so that the graphic designer can work faster =) Nothing beats pure Velocity for that requirement.

The desire I have had to think of page submissions as event callbacks is pretty well met by FlowScript. Page submissions are now function calls, and even more nicely they are re-entrant because of the elegance of the continuation implementation. You can drop out of one function and come back to it later. This solves the menu problem (people can go anywhere from most pages, sites are not wizards with a single possible execution path.

Woody... woody is interesting and needs to mature some, but it works fine as long as you don't mind hacking xslt some to get things the way you want them. Whoever does the graphic design needs to be pretty technically competent though or it will be a pita.

6 writebacks [/src/java] permanent link