ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


XML Publishing with Cocoon 2, Part 2

by Collin VanDyck and David Cummings
07/16/2003

In our previous article, we covered the basics of sitemap navigation in Cocoon (the Cocoon XML publishing framework process), creating custom XML generators through the use of XSP, and using the XSP to homogenize the look and feel of sitewide content, specifically forms.

Picking up from where we left off, we had just generated a login form that submitted a username and password. Typically, all of our forms submit data back into Cocoon and to a URI that matches code in the sitemap that invokes a Cocoon Action. Actions are simply Cocoon components that implement org.apache.cocoon.acting.Action. Specifically,

 

public java.util.Map act(Redirector redirector,
        SourceResolver resolver,
        java.util.Map objectModel,
        java.lang.String source,
        org.apache.avalon.framework.parameters.Parameters par)
   throws java.lang.Exception;

must be implemented. From the parameter list, you can see that the environment objectModel is passed in, from which you can derive the request object and also any sitemap parameters that are passed in. Information is passed into the action through the request and the sitemap parameters. Sitemap parameters are denoted through XML elements in the sitemap. For example:

<map:act type="login-action">
  <map:parameter name="session-timeout" "30"/>
</map:act>

In the code, the Action would receive the parameter as:

try {
        String timeoutStr = par.getParameter("session-timeout");
        this.timeout      = new Integer(timeoutStr);
} catch (ParameterException e) {
        log.info("Did not get a parameter for the timeout. Using default");
        this.timeout = TIMEOUT_DEFAULT;
}

Sitemap parameters must come directly after the use of the component. Much like matchers, you can see that the Action returns a Map. This object is non-null if the Action was successful, and null if the Action failed in any way. In the same way as the matcher, the sitemap nesting reflects this if-then-else logic.

The other way we send information to Actions is through the request object. When a form is submitted, the form inputs are encoded as request parameters. You can derive the request object through the objectModel:

Request request = ObjectModelHelper.getRequest(objectModel);

From here, you can get request parameters as expected:

String login    = request.getParameter("login");
String password = request.getParameter("password");

We use a combination of both sitemap parameters and request parameters in our Actions. Typically, we will use sitemap parameters to denote functionality, and the request parameters will contain the actual data with which to work.

Here's what your LoginUserAction act() might look like:

public Map act
    (Redirector redirector, SourceResolver resolver, Map objectModel,
    String src, Parameters par) throws Exception {
    try {
        String timeoutStr = par.getParameter("session-timeout");
        this.timeout      = new Integer(timeoutStr);
    } catch (ParameterException e) {
        log.info("Did not get a parameter for the timeout. Using default");
        this.timeout = TIMEOUT_DEFAULT;
    }

    Request req     = ObjectModelHelper.getRequest(objectModel);
    Session session = req.getSession();

    // get user parameters from the request
    String name     = req.getParameter("name");
    String password = req.getParameter("password");

    if (name == null || password == null) {
        // this should never happen..
        return null;
    }
    Principal myPrincipal = null;
    myPrincipal           = login(name, password); // our own function.

    if (!(myPrincipal == null)) {
        session.setAttribute("principal", myPrincipal);
        session.setMaxInactiveInterval(this.timeout);
        Map sitemapParams = new HashMap();
        return sitemapParams;
    } else {
        return null;
    }
}

Message Handling with Actions

Every web application should give some sort of visual feedback to the user once something of significance has happened; in our case, the user has logged in. Let's say our user should then receive a message saying so. We designed our application such that all important things were accomplished through our use of Actions (which delegated some tasks to our J2EE back end as well). Because of this, we decided that it made a lot of sense to enable our Actions to pass messages back up to our GUI front end.

In order to pass messages, we insert objects into the Cocoon Request object. You saw earlier how every Action has access to the Request object through the object model. The great thing about the Request object is that you may place arbitrary numbers and types of objects into it as key-value pair attributes of that Request:

request.setAttribute("message","You have just logged in!");

However, just passing a String as a message doesn't really seem flexible enough for our needs. We need to be able to send multiple messages out of a Request. For example, what if the user did not fill out three of the form fields correctly?. To that effect, we created a wrapper class around a Vector, called Messages, which supported addMessage() and addError(), as well as a way to iterate through those messages and errors.

In this scenario, we might send a message like this:

Messages messages = new Messages();
messages.addMessage("You have just logged in!");
request.setAttribute("messages",messages);

This is more flexible, but we still would like an easier way for Actions to be able to use messaging in our application. Abstraction wins again, resulting in two classes:

All of our messaging actions simply inherit from AbstractErrorAction, and implement the act() method. Our two abstract messaging classes define these methods:

error(String error);
commitErrors(Request req);
message(String message);
commitMessages(Request req);

The abstract classes handle the instantiation of the Messages wrapper and other overhead. Any class that inherits from these could simply call error() to its heart's content, committing those errors into the request object with commitErrors() before returning null. Because the Request object is visible throughout the life of the request-response lifecycle, the GUI will be able to retrieve the messages from the Request object and display them accordingly.

Our code above, with messaging implemented, would look like this:

public Map act
    (Redirector redirector, SourceResolver resolver, Map objectModel,
    String src, Parameters par) throws Exception {
    try {
        String timeoutStr = par.getParameter("session-timeout");
        this.timeout = new Integer(timeoutStr);
    } catch (ParameterException e) {
        log.info("Did not get a parameter for the timeout. Using default");
        this.timeout = TIMEOUT_DEFAULT;
    }

    Request req     = ObjectModelHelper.getRequest(objectModel);
    Session session = req.getSession();

    // get user parameters from the request
    String name     = req.getParameter("name");
    String password = req.getParameter("password");

    if (name == null || password == null) {
        // this should never happen..
        error("Unexpected error: received null parameters");
        commitErrors(req);
        return null;
    }

    Principal myPrincipal = null;
    myPrincipal           = login(name, password); // our own function.

    if (!(myPrincipal == null)) {
        session.setAttribute("principal", myPrincipal);
        session.setMaxInactiveInterval(this.timeout);
        message(name + " successfully logged in");
        commitMessages(req);
        Map sitemapParams = new HashMap();
        return sitemapParams;
    } else {
        error("Could not log " + name + " in");
        commitErrors(req);
        return null;
    }
}

Source Factories in Cocoon

We saw earlier how to generate XML SAX events from a file containing XML, and then how to dynamically generate those SAX events using XSP and embedded Java code. However, there are times when we need to generate more complex XML. An example of this would be the search results page: search criteria are specified, and the system must build an XML tree of the search results. This would be difficult with XSP, to say the least.

Cocoon Configuration

Before we get into how to do this, let's quickly visit the world of Cocoon configuration. You'll find in the Cocoon distribution a file called cocoon.xconf in WEB-INF/ that defines the configuration parameters for Cocoon.

In this file, you may define new protocols for what is called the source handler. Don't worry too much about the source handler right now. Here's the excerpt for the source handler in cocoon.xconf:

<source-handler logger="core.source-handler">
    <!-- file protocol : this is a WriteableSource -->
    <protocol
        class="org.apache.cocoon.components.source.FileSourceFactory"
        name="file"/>
    <!-- contentxml pseudo protocol -->
    <protocol
        class="com.hannonhill.cocoon.components.source.ContentXMLSourceFactory"
        name="contentxml"/>
    <!-- xmldb pseudo protocol -->
    <protocol class="org.apache.cocoon.components.source.XMLDBSourceFactory" 
	    name="xmldb">
      <!-- Xindice driver -->
      <driver class="org.apache.xindice.client.xmldb.DatabaseImpl" type="xindice"/>
      <!-- Add here other XML:DB compliant databases drivers -->
    </protocol>
</source-handler>

You can see that we have added our own class, ContentXMLSourceFactory, as the handler for the contentxml pseudo-protocol. This allows us to use the following in the sitemap:

<map:match pattern="getEntityXML/*">
        <map:generate src="contentxml:entity-xml://{1}"/>
        <map:serialize type="xml"/>
</map:match>

This is a simple example for illustration purposes. We have many different types of content, and each type of content may be represented as XML. Sending the entity-xml message to our contentxml protocol should generate the XML for that entity. You might ask, which entity? See the variable interpolation after the ://? That's where we pass in our extra information, if any, to our source generators.

Note that we could have defined our protocol as:

contentxml:entity-xml:{1}:

if we wished.

Delegating from the SourceFactory

Your source factory must implement org.apache.cocoon.components.source.SourceFactory, which mandates this particular method:

org.apache.cocoon.environment Source
         getSource(Environment environment, java.lang.String location);

The string that we formed our generate call with is passed in as the location parameter. Essentially, you would then check for each possible message in the SourceFactory:

String action = parseAction(location); // defined elsewhere.
if action.equals("entity-xml") {
    return new EntityXMLSource(...);
} else if (action.equals("search-results")) {
    return new SearchResultsSource(...);
}

Creating Source Objects from the Factory

True to the definition of a factory, our extension of SourceFactory is simply responsible for parsing the input message, and returning a Source object. This is a Cocoon object that implements the Source interface, inheriting the toSAX() method:

public void toSAX(ContentHandler handler)
        throws SAXException, ProcessingException;

This is where the heart of the functionality for the Source happens. Most all of the other methods are responsible for setting up the object, and once all of that is finished, the object is ready for Cocoon to invoke the toSAX() method. Cocoon supplies the ContentHandler to this method. You can think of that as the object that receives the SAX events that this Source object will generate. This object could be a transformer, or possibly a serializer. That's out of the control of this component, so we'll not address it. All this component knows is that it must generate some XML, parse it, and send the SAX events from that XML to the ContentHandler supplied in the toSAX() method. Whew!

We've found JDOM to be very useful in our XML generation. JDOM is essentially a way of building an XML document outside of DOM and SAX. It provides ways of integrating with both DOM and SAX, and its API is extremely simple to use. Our algorithm then looks like this:

  1. toSAX() is called by Cocoon.
  2. Our Source object gathers information from the database, and constructs a JDOM tree.
  3. We create SAX events from our JDOM tree and send them to the ContentHandler.

In Java, this looks like:

public void toSAX(ContentHandler handler)
    throws SAXException, ProcessingException {
    try {
        // create a jdom outputter
        SAXOutputter outputter = new SAXOutputter(handler);

        // defined elsewhere
        Document stylesheetDocument = getObjectListAsJDOM();

        // send our document as SAX events
        outputter.output(stylesheetDocument);
    } catch (JDOMException e) {
        throw new ProcessingException("Error outputting XML: " + e);
    } catch (RemoteException e) {
        throw new ProcessingException("Remote error outputting XML: " + e);
    }
}

Neat! With the simplicity of our Sources, the difficult part here is to build the JDOM document. That, unfortunately, is outside of the scope of this tutorial, but you can find documentation, including Javadocs, at jdom.org.

Summary

Cocoon provides a true XML publishing framework that is very powerful and pluggable with custom components. Learning Cocoon takes a decent amount of time, but the rewards are enormous. I hope this quick technical overview of Cocoon, gained through experiences with our ContentXML product, can help you with your next XML publishing application.

Collin VanDyck is the lead developer of ContentXML and an integral part of the Hannon Hill team.

David Cummings is the CEO of Hannon Hill Corporation which focuses on content management software solutions.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.