Apache Forrest WikiRenderer
 
   

XMLFormXindiceOldVersion

PDF
PDF

Overview

This How-To shows you how to use Xindice as the repository for XML resources from the XMLForm Framework. It requires prior knowledge of Cocoon XMLForm, XSLT, Schematron, Xindice and the XMLDB API.

Purpose

You will learn how to build a simple wizard type XMLForm that stores XML data into a Xindice collection.

Intended Audience

Cocoon users who want to learn how to store data obtained from XMLForms into Xindice.

Prerequisites

Cocoon must be running on your system. The steps below have been tested with Cocoon 2.1-dev. You will need the following:

  • A servlet engine such as Tomcat.
  • JDK 1.2 or later
  • Xindice 1.0 installed (create a collection named

Artist)

  • Cocoon 2.1 CVS to be installed with the command:
build -Dinclude.webapp.libs=true webapp

You will need to understand and be familiar with XSL, XForms, XPath, Schematron and Xindice. Some knowledge about the XMLDB API would be helpful, too. If you are unfamiliar with these technologies, it is advised that you learn these related concepts first. If you are unfamiliar with XMLForm, check out the XMLForm Wizard How-Tofirst.

Steps

We will follow the needed steps in order to add a document like this:

<Artist id="pearljam">
  <Name>Pearl Jam</Name>
</Artist>

to the Xindice root collection. We will get the identifier and name data using a XMLForm and store them in Xindice. We will build this XMLForm very similar to the one in the XMLForm Wizard How-To.

Building the XMLForm files

Create the files and name them as specified below.

start.xform

<?xml version="1.0"?>
<document>
  <h1>This is the New Artist Wizard!</h1>
  <info>Steps from here on, will let you insert a new
        Artist in the database.
  </info>
  <h3>
    <a href="Artist.xform?cocoon-action-start=true">
       Start!
    </a>
  </h3>
</document>

artist.xform

<?xml version="1.0"?>
<document
 xmlns:xf="http://xml.apache.org/cocoon/xmlform/2002">
  <xf:form id="form-insert" view="artist"
   action="Artist.xform" method="post">
    <xf:caption>New Artist</xf:caption>
      <error>
        <xf:violations class="error"/>
      </error>
    <xf:textbox ref="/artistDocument/@id">
      <xf:caption>Artist identifier:</xf:caption>
    </xf:textbox>
    <xf:textbox ref="/artistDocument/Name">
      <xf:caption>Artist Name:</xf:caption>
    </xf:textbox>
    <xf:submit id="prev" class="button">
      <xf:caption>Prev</xf:caption>
      <xf:hint>Go to previous page</xf:hint>
    </xf:submit>
    <xf:submit id="next" class="button">
      <xf:caption>Next</xf:caption>
      <xf:hint>Go to next page</xf:hint>
    </xf:submit>
  </xf:form>
</document>

end.xform

<?xml version="1.0"?>
<document>
  <h1>You have reached the last page!</h1>
  <info>
   You have inserted a New Artist successfully.
  </info>
  <h3>
    <a href="Artist.xform">Go to home page.</a>
  </h3>
</document>

error.xform

<?xml version="1.0"?>
  <document>
    <h1>
      You have reached the last page of the New Artist Wizard!
    </h1>
    <info>
      There have been problems and the Artist could not be added to the database.
      Please try again.
    </info>
    <h3>
      <a href="Artist.xform">Please, start again.</a>
    </h3>
</document>

Validation

For the sake of simplicity we just validate one property against one condition. We require the identifier to be at least two characters in length; the validation file, artist-validator.xmlis as follows:

<?xml version="1.0"?>
<schema ns="http://xml.apache.cocoon/xmlform" 
 xmlns="http://www.ascc.net/xml/schematron">
  <phase id="artist">
    <active pattern="artval"/>
  </phase>
  <pattern name="Artist Identifier Validation" 
   id="artval">
    <rule context="/artistDocument/@id">
      <assert test="string-length(.) &gt; 1">
        Artist Name should be at least 2 characters.
      </assert>
    </rule>
  </pattern>
</schema>

Extended Validation

There could be more complicated rules in Schematron but we could also require the identifier to be unique in the database. In this case we should query the database and see if it already exists. If so, a new violation can be added to the form. Since these kinds of violations are out of the scope of Schematron, these operations should be accomplished in the Action using Java code.

Model Bean

Persistence for the data will be accomplished by this bean. In order to store the mentioned XML structure we will use a DOM Node. You can also try JXPath Containers as suggested by Ivelin. The model bean ArtistBean.javais as follows:

package com.simbiosystems.cocoon.xmlform.xindice.howto;

import java.util.ArrayList;
import java.util.List;

import org.w3c.dom.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;

/**
 *
 * A sample domain object used as a Form model.
 * DOM Nodes, are handled correctly by the
 * framework when referenced via JXPath.
 *
 */
public class ArtistBean {

  //Holds the DOM Node that will be filled by the form and
  //added later to Xindice
  private Node artistDocument;

  public ArtistBean () {
    //create the document model
    initModel();
  }

 /**
  * Creates an empty DOM Node which holds an empty copy
  * of an Artist document like this:
  * <Artist id="">
  *   <Name></Name>
  * </Artist>
  * The data from the form is stored in it.
  */
  public void initModel() {

    DOMImplementation impl;
    try {
      // Find the implementation
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      factory.setNamespaceAware(true);
      factory.setValidating ( false );
      DocumentBuilder builder = factory.newDocumentBuilder();
      impl = builder.getDOMImplementation();
    }
    catch (Exception ex) {
      throw new RuntimeException("Failed to initialize DOM factory. Root cause: \n" + ex);
    }
    //You can choose any altenative method for this...
    //Create the node for the root, 'Artist'
    Document doc = impl.createDocument( null, "Artist", null);
    Node artistRoot = doc.getDocumentElement();

    //add a 'id' attribute
    Attr id = doc.createAttribute ( "id" );
    id.setValue ( "" );
    NamedNodeMap nmap = artistRoot.getAttributes();
    nmap.setNamedItem ( id );

    //add a 'Name' child
    Node artistName = doc.createElement ( "Name" );
    Text text = doc.createTextNode( "" );
    description.appendChild(text);
    artistRoot.appendChild( artistName );

    artistDocument = artistRoot;
  }

  /**
   * Gets the artistDocument
   * @return Returns a Node
   */
  public Node getArtistDocument() {
    return artistDocument;
  }

  /**
   * Sets the artistDocument
   * @param artistDocument The artistDocument to set
   */
  public void setArtistDocument(Node artistDocument) {
    this.artistDocument = artistDocument;
  }
}

The Action

In this Action we have integrated the Xindice handling code, getting the data from the model (from the DOM Node property) and storing it in Xindice. The Action that controls this form is ArtistAction.java:

package com.simbiosystems.cocoon.xmlform.xindice.howto;

import java.util.Arrays;
import java.util.Enumeration;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.avalon.framework.parameters.Parameters;
import org.apache.cocoon.Constants;
import org.apache.cocoon.acting.AbstractXMLFormAction;
import org.apache.cocoon.components.validation.Violation;
import org.apache.cocoon.components.xmlform.Form;
import org.apache.cocoon.components.xmlform.FormListener;
import org.apache.cocoon.environment.Redirector;
import org.apache.cocoon.environment.Request;
import org.apache.cocoon.environment.Session;
import org.apache.cocoon.environment.SourceResolver;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Node;

import com.simbiosystems.cocoon.xmlform.xindice.howto.XindiceManager;

/**
 * This action handles XMLForms for the Artist data
 */
public class ArtistAction extends AbstractXMLFormAction implements FormListener {
  // different form views participating in the form
  final String VIEW_START = "start";
  final String VIEW_ARTIST = "artist";
  final String VIEW_END = "end";
  final String VIEW_ERROR = "error";
  // action commands used in the wizard
  final String CMD_START = "start";
  final String CMD_NEXT = "next";
  final String CMD_PREV = "prev";
  //constants for the XML manipulation, it holds the full name of the collection
  //to be used for storing the data
  final String xindiceSubCol = "/Artist";
  /**
   * The first callback method which is called
   * when an action is invoked.
   *
   * It is called before population.
   * @return null if the Action is ready to continue.
   * an objectModel map which will be returned
   */
  protected Map prepare() {
    if ( getCommand() == null ) {
      return page(VIEW_START);
    }
    else if ( getCommand().equals(CMD_START)) {
      // reset state by removing old form if one exists
      Form.remove( getObjectModel(), getFormId() );
      getForm().addFormListener( this );
      return page(VIEW_ARTIST);
    }
    // get ready for action
    // if not ready return page("whereNext");
    return null;
  }
  /**
   * Invoked after form population
   * Take appropriate action based on the command
   *
   */
  public Map perform () {
    // get the model which this Form encapsulates
    // and apply logic to the model
    ArtistBean jBean=(ArtistBean)getForm().getModel();
    // set the page control flow parameter
    // according to the validation result
    if ( getCommand().equals(CMD_NEXT) &&
         getForm().getViolations () != null ) {
      // errors, back to the same page
      return page( getFormView() );
    }
    else {
      // validation passed
      // continue with control flow
      // clear validation left overs in case the user
      // did not press the Next button
      getForm().clearViolations();
      // get the user submitted command
      String command = getCommand();
      // get the form view which was submitted
      String formView = getFormView();
      // apply control flow rules
      if (formView.equals (VIEW_ARTIST)) {
        if (command.equals(CMD_NEXT)) {
          //extended validation
          //test if the ID already exists in the DB
          Node artistName = jBean.getArtistDocument().getAttributes().getNamedItem("id");
          try {
            XindiceManager xi = new XindiceManager();
            Node result = xi.find(xindiceSubCol, "//Artist[@id='"+ artistName.getNodeValue() +"']", "Artists");
           //if we do not get a null the element with that ID
           //already existed we add the violation
            if (result!=null) {
              Violation v = new Violation();
              v.setMessage("already exists in the database, please choose another one");
              v.setPath("/artistName");
              Violation[] va = { v };
              getForm().addViolations(Arrays.asList((Object[])va));
             //the ID already exists, back to the same
             //page to correct the error
              return page(VIEW_ARTIST);
            }
          }
          catch (Exception e) {
            getLogger().error("Cannot establish a connection to the DB", e);
          }
          //everything went fine, add the document to the database
          try {
             createDocument(jBean);
          }
          catch(Exception e) {
         //there were errors, send it to the error page
           getLogger().error("Cannot add DOM document to the database");
            return page(VIEW_ERROR);
          }
          return page( VIEW_END);

       }
        if (command.equals(CMD_PREV)) {
          return page(VIEW_START);
        }
      }
    }
  }

  /**
   * FormListener callback
   * called in the beginning Form.populate()
   * before population starts.
   *
   * This is the place to handle unchecked checkboxes.
   *
   */
  public void reset( Form form ) {
    // based on the current form view
    // make some decisions regarding checkboxes, etc.
    String formView = getFormView();
  }

  /**
   * Inserts the document into the database
   */
  public void createDocument(ArtistBean jBean)
              throws Exception {
    try {
      //add the document to the database
      XindiceManager xi = new XindiceManager();
      xi.add(xindiceSubCol, jBean.getArtistDocument(), null);
    }
    catch (Exception e) {
      getLogger().error("DOM Document could not be created", e);
      throw e;
    }
  }
}

The helper class

In order to make this work we need to use a helper class. This class uses the XMLDB to connect to Xindice and make the operations available to the Action. You should extend it to add more operations. The helper class XindiceManager.javais as follows:

/**
 * Helper class for Xindice related operations
 *
 */
package com.simbiosystems.cocoon.xmlform.xindice.howto;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xmldb.api.DatabaseManager;
import org.xmldb.api.base.Collection;
import org.xmldb.api.base.Database;
import org.xmldb.api.base.Resource;
import org.xmldb.api.base.ResourceIterator;
import org.xmldb.api.base.ResourceSet;
import org.xmldb.api.base.XMLDBException;
import org.xmldb.api.modules.XMLResource;
import org.xmldb.api.modules.XPathQueryService;

public class XindiceManager {

  private static final String driver = "org.apache.xindice.client.xmldb.DatabaseImpl";
  private static final String rootCollection = "xmldb:xindice:///db/";

  /**
   * Constructor
   *
   */
  public XindiceManager() {

  }

  /**
   * Search for a document in the DB. If not found, return null.
   *
   * @param subCol name of the subCollection to query if any. If blank or null,
   * queries go against the rootCollection.
   * @param xpath XPath expression for the query, if none, it returns the whole Collection
   * @param resultRootelement name of the root element for the DOM document which will 
   * wrap the results
   * @return a DOM Node with the matched documents
   *
   */
  public Node find(String subCol, String xpath, String resultRootElement)
    throws Exception {

    //prepare DOM document
    DOMImplementation impl;
    DocumentBuilder builder;
    try {
      // Find the implementation
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      factory.setNamespaceAware( false );
      factory.setValidating ( false );
      builder = factory.newDocumentBuilder();
      impl = builder.getDOMImplementation();
    }
    catch (Exception ex) {
      throw new RuntimeException("[XindiceManager.find]: Failed to initialize DOM factory. Root cause: \n" + ex);
    }
    //create the Document which will hold the results
    Document resultDoc = impl.createDocument(null, resultRootElement, null);
    Node root = resultDoc.getDocumentElement();

    //And now the Xindice part

    Collection col = null;
    String strData = null;

    try {
      Class c = Class.forName(driver);

      Database database = (Database) c.newInstance();
      DatabaseManager.registerDatabase(database);

      String localCol = rootCollection + subCol;

      col = DatabaseManager.getCollection(localCol);

      //Make the XPath query and get the resources
      XPathQueryService service =
        (XPathQueryService) col.getService("XPathQueryService", "1.0");
      ResourceSet resultSet = service.query(xpath);
      // Iterate the xpath results and add each of them to the main Document
      ResourceIterator iterator = resultSet.getIterator();

      //if not resources are present, just return null
      if(!iterator.hasMoreResources()) return null;

      while(iterator.hasMoreResources()) {
        Resource r = iterator.nextResource();

        Element resElement = ((Document)((XMLResource)r).getContentAsDOM()).getDocumentElement();

        // Remove unwanted attributes
        resElement.removeAttribute("src:col");
        resElement.removeAttribute("src:key");
        resElement.removeAttribute("xmlns:src");

        // Add this result to the root element
        Element importedElement = (Element)resultDoc.importNode(resElement, true);
        root.appendChild(importedElement);
      }
      //return the Document root
      return root;
    }
    catch (Exception e) {
      System.err.println("[XindiceManager.find]: Find Exception occured :" + e.getMessage());
      throw e;
    }
    finally {
      if (col != null) {
        col.close();
      }
    }
  }

  /**
   * Add a document to the DB. If a key is not passed, it generates a unique one.
   * This version uses a DOM Document.
   * @param subCol name of the subCollection in which to add the resource if any. If blank or 
   * null, insertions go against the rootCollection.
   * @param document DOM document to be inserted
   * @param key unique key for the resource to be created, if none, one is created on the fly
   */
  public void add(String subCol, Node document, String key) throws Exception {
    Collection col = null;
    Node xmldoc = null;
    try {
      Class c = Class.forName(driver);

      Database database = (Database) c.newInstance();
      DatabaseManager.registerDatabase(database);

      col = DatabaseManager.getCollection(rootCollection + subCol);

      XMLResource document_ = (XMLResource) col.createResource(null, "XMLResource");
      document_.setContentAsDOM(document);
      col.storeResource(document_);
    }
    catch (XMLDBException e) {
      System.err.println("XML:DB - Add Exception occured " + e.errorCode);
      throw e;
    }
    finally {
      if (col != null) {
        col.close();
      }
    }
  }
}

The Sitemap

Remember you should have the XMLFormTransformer defined like this:

<map:transformer name="xmlform" src="org.apache.cocoon.transformation.XMLFormTransformer" 
logger="xmlform"/>

Then, you must declare the Action to be used in the correspondent section:

<map:action logger="xmlform" name="ArtistAction" src="com.simbiosystems.cocoon.xmlform.xindice.howto.ArtistAction"/>

The Pipeline

<!-- XMLForms pipeline -->
<map:pipeline>
 <map:match pattern="**/*.xform">
   <map:act type="ArtistAction">
   <map:parameter name="actionName" value="{2}Action"/>
     <!-- XMLForm parameters for the Action -->
     <map:parameter name="xmlform-validator-schema-ns" value="http://www.ascc.net/xml/schematron"/>
     <map:parameter name="xmlform-validator-schema" value="{1}/{2}/schematron/artist-validator.xml"/>
     <map:parameter name="xmlform-id" value="form-insert"/>
     <map:parameter name="xmlform-scope" value="session"/>
     <map:parameter name="xmlform-model" 
      value="com.simbiosystems.cocoon.xmlform.xindice.howto.ArtistBean"/>
     <!-- XMLForm document, {page} comes from Action -->
     <map:generate src="{../1}/{../2}/{page}.xml"/>
   </map:act>
   <!-- populating the doc with model instance data -->
   <map:transform type="xmlform"/>
   <!-- look and feel of the form controls  -->
   <map:transform src="styles/wizard2html.xsl"/>
   <!-- Transforming the XMLForm controls to HTML -->
   <map:transform src="styles/xmlform2html.xsl"/>
   <!-- sending the HTML back to the browser -->
   <map:serialize type="html"/>
  </map:match>
</map:pipeline>

Depending on where you have Cocoon installed and where you have configured the files in this howto, you could make a request to a URL like http://localhost:8080/cocoon/Artist.xformand start using the Form.

Final Considerations

Making the operations from the Action using the helper class seems to be the easiest way. You can think of other ways or other operations using other Xindice tools available in Cocoon such as the pseudo protocol. For example, you could use it to query the DB for data that could be stored in a sitemap parameter. You could then get the data from the Action and use it to fill a selectbox in the form.\\ We did not mentioned other operations such as updates. This can be also accomplished this way. For example, if you want to edit the data we just stored, you could load it in the DOM Node at the beginning by using the findmethod of the helper class. This way you get a filled form in the next step ready for editing. Other ways or interacting with the repository include the XMLDB Transformer. Since it uses XUpdate alike syntax, you could format a XML String for it in the last step of the Action, making it available to the sitemap then, so the Transformer could get it and make the operations.\\ I'm sure you can think of more different ways. I encourage you to contribute them if you have tested it succesfully.

Summary

This How-To makes possible the use of Xindice from XMLForms in order to add data to the repository. You learned how to connect to the DB from the Action, and how to make complex validation using information stored in the DB. I hope it was helpful for you.