Hi All

At the GT, I got the chance to meet Antonio (oh joy !!) and one of the many things we talked about was the possibility of him giving me some help learning Apache ORO. He very graciously accepted!!!

I have a project, the o.a.c.bean.query.SimpleLuceneQuery, in Cocoon, which is possibly an ideal candidate, as it comes out of a project for work and I have already made it persistable, using Hibernate.

What I hope to do is to make the Queries persistable in HSQLDB via ORO and add this to Cocoon as a new Block, both as a sample and as a useful module that should be easy to add to your own project.

Antonio's recommendation was that we discuss this conversion on the Dev list, as other members of the list could also benefit from this.

So, unless anyone has any objections ...... here goes !!!

Background:

The QueryBean is a simple Bean for allowing the User to assemble complex Queries on a Lucene index via CForms, without having to know the Lucene Query Language. Try it, it is in the Lucene Samples. I have actually completely re-written the flowscript in my local version to be much cleaner, but the current version in Cocoon allows you to do the following:

Quick Queries: a simple query assembled from request params
Complex Queries: a CForms repeater to allow multiple Queries and'd or or'd together
Multiple match types (like, contains, exact etc.).
Easy to setup field Queries.
Query History kept in your Session, you can re-use and edit previous Queries


Structure:

A Query is made up of a single SimpleLuceneQueryBean, which has at least one SimpleLuceneCriterionBean(s) in it's Collection.

The (currently PostGres) DB Schema is very simple, it looks like this:

CREATE TABLE query (
id serial unique, /* the unique persistence ID */
user_id text NOT NULL, /* the ID of the owner */
q_date timestamp, /* the date it was saved */
q_bool text, /* how the criteria are combined */
q_name text, /* the name of the saved query */
q_type text, /* the type of the saved query (ie. which CForm) */
q_size integer /* the number of results to show per page */
);


CREATE TABLE criterion (
id serial unique, /* the unique persistence ID */
query_id integer, /* the query this belongs to */
c_field text, /* the lucene field to search in */
c_match text, /* the type of match to perform */
c_term text, /* the term to search for */
c_position integer, /* the index of this criterion in the query's criteria */
foreign key ("query_id") references query("id")
);


GRANT INSERT, SELECT, UPDATE, DELETE ON query, query_id_seq, criterion, criterion_id_seq TO "cocoon";

As you can see from this, there are a bunch of text fields to hold info about the query, and a one-to-many relationship between the Query and the Criterion(s). If you compare this with the current state of the Beans in Cocoon, the following changes have been made:

Added the property 'user' to the Query so we can see which are yours
Changed the name of the property that hold the query text in the Criterion from 'value' to 'term' to make it clearer.


Mapping:

Below, is the Hibernate Mapping for the Query and Criterion Beans, adjusted to the Cocoon Package. As you can see, it is about as simple as you can get.

<hibernate-mapping package="org.apache.cocoon.bean.query">
<class name="SimpleLuceneQueryBean" table="query">
<id name="id" column="id" type="long">
<generator class="sequence">
<param name="sequence">query_id_seq</param>
</generator>
</id>
<property name="bool" column="q_bool" type="string"/>
<property name="date" column="q_date" type="timestamp"/>
<property name="name" column="q_name" type="string"/>
<property name="type" column="q_type" type="string"/>
<property name="size" column="q_size" type="long"/>
<property name="user" column="user_id" type="string"/>
<list name="criteria" table="criterion" cascade="all-delete-orphan" lazy="false">
<key column="query_id"/>
<index column="c_position"/>
<one-to-many class="SimpleLuceneCriterionBean"/>
</list>
</class>
</hibernate-mapping>


<hibernate-mapping package="org.apache.cocoon.bean.query">
  <class name="SimpleLuceneCriterionBean" table="criterion">
    <id name="id" column="id" type="long">
      <generator class="sequence">
        <param name="sequence">criterion_id_seq</param>
      </generator>
    </id>
    <property name="field" column="c_field" type="string"/>
    <property name="match" column="c_match" type="string"/>
    <property name="term" column="c_term" type="string"/>
  </class>
</hibernate-mapping>

The FlowScript:

This is the QueryFavourites JavaScript Object Library that handles most of the work, you will see that it uses our PersistanceFactory to manage the connection to PostGres. The error messages it outputs are i18n keys.

importClass(Packages.net.sf.hibernate.expression.Expression);
importClass(Packages.net.sf.hibernate.expression.Order);
importPackage(Packages.org.apache.cocoon.bean.query);

// QueryFavourites constructor
function QueryFavourites(user) {
this._user = user;
this._factory = cocoon.getComponent(co.uk.our.PersistanceFactory.ROLE);
}


// add a Query to the QueryFavourites
QueryFavourites.prototype.add = function(query) {
  query.user = this._user;
  query.date = new java.util.Date();
  var session = null;
  var tx = null;
  try {
    session = this._factory.createSession();
    tx = this._factory.startTransaction(session);
    var id = session.save(query);
    this._factory.endTransaction(tx);
  } catch (error) {
    cocoon.log.error(error);
    this._factory.undoTransaction(tx);
    throw (error);
  } finally {
    session.close();
  }
}

// remove a Query from the QueryFavourites, using the favourites ID
QueryFavourites.prototype.remove = function(id) {
  var session = null;
  var tx = null;
  var i;
  try {
    i = new java.lang.Long(id)
  } catch (error) {
    cocoon.log.error(error);
    throw("error.no.favourite");
  }
  try {
    session = this._factory.createSession();
    tx = this._factory.startTransaction(session);
    var query = session.load(SimpleLuceneQueryBean, i);
    if (query != null) {
      session["delete"](query);
    } else {
      throw ("error.no.favourite");
    }
    this._factory.endTransaction(tx);
  } catch (error) {
    cocoon.log.error(error);
    this._factory.undoTransaction(tx);
    throw (error);
  } finally {
    session.close();
  }
}

// get a Query from the QueryFavourites using the favourites ID
QueryFavourites.prototype.get = function(id) {
  var session = null;
  var i;
  try {
    i = new java.lang.Long(id)
  } catch (error) {
    cocoon.log.error(error);
    throw("error.no.favourite");
  }
  try {
    session = this._factory.createSession();
    var query = session.load(SimpleLuceneQueryBean, i);
    if (query == null) {
      throw("error.no.favourite");
    } else {
      return (query);
    }
  } catch (error) {
    cocoon.log.error(error);
    throw (error);
  } finally {
    session.close();
  }
}

// get a list of Queries from the QueryFavourites
QueryFavourites.prototype.list = function() {
  var session = null;
  try {
    session = this._factory.createSession();
    var query = session.createCriteria(SimpleLuceneQueryBean)
      .add(Expression.eq("user", this._user))
      .addOrder(Order.desc("date"));
    return (query.list());
  } catch (error) {
    cocoon.log.error(error);
    throw (error);
  } finally {
    session.close();
  }
}

// close the QueryFavourites
QueryFavourites.prototype.close = function() {
        cocoon.releaseComponent(this._factory);
}

Next, we have the FlowScript that is called from the Sitemap:

// display the User's Favourite Searches
function showFavourites() {
var favourites = null;
try {
favourites = new QueryFavourites(cocoon.parameters["user-id"]);
cocoon.sendPage(cocoon.parameters["screen"], {queries: favourites.list()});
} catch (error) {
cocoon.log.error(error);
cocoon.sendPage("screen/error", {message: error});
} finally {
if (favourites != null) favourites.close();
}
}


// add a history item to the User's Favourite Searches, using the History ID
function addFavourite() {
var history = new QueryHistory(cocoon.parameters["history"]);
var favourites = null;
try {
favourites = new QueryFavourites(cocoon.parameters["user-id"]);
var query = history.get(cocoon.parameters["hid"]);
if (query != null) {
favourites.add(query);
}
cocoon.sendPage(cocoon.parameters["screen"], {queries: favourites.list()});
} catch (error) {
cocoon.log.error(error);
cocoon.sendPage("screen/error", {message: error});
} finally {
if (favourites != null) favourites.close();
}
}


// remove an item from the User's Favourite Searches, using it's ID
function removeFavourite() {
var favourites = null;
try {
favourites = new QueryFavourites(cocoon.parameters["user-id"]);
favourites.remove(cocoon.parameters["fid"]);
cocoon.sendPage(cocoon.parameters["screen"], {queries: favourites.list()});
} catch (error) {
cocoon.log.error(error);
cocoon.sendPage("screen/error", {message: error});
} finally {
if (favourites != null) favourites.close();
}
}


And now some selected bits from the Sitemap:


<!-- list favourites -->
<map:match pattern="favourites.html">
<map:call function="showFavourites">
<map:parameter name="screen" value="screen/favourites"/>
<map:parameter name="user-id" value="{session-context:authentication/authentication/data/ldap/uid}"/>
</map:call>
</map:match>


<!-- add a history item to the favourites, using the history ID -->
<map:match pattern="add-favourite.html">
<map:call function="addFavourite">
<map:parameter name="screen" value="screen/favourites"/>
<map:parameter name="user-id" value="{session-context:authentication/authentication/data/ldap/uid}"/>
<map:parameter name="hid" value="{request-param:hid}"/>
<map:parameter name="history" value="{global:history}"/>
</map:call>
</map:match>


<!-- remove an item from the favourites, using the favourite ID -->
<map:match pattern="remove-favourite.html">
<map:call function="removeFavourite">
<map:parameter name="screen" value="screen/favourites"/>
<map:parameter name="user-id" value="{session-context:authentication/authentication/data/ldap/uid}"/>
<map:parameter name="fid" value="{request-param:fid}"/>
</map:call>
</map:match>


Phew!!!!

I have left lots of bits out, but hopefully none of them are relevant to this discussion.

I suppose one of the first questions that needs asking before going ahead and making an Apache ORO equivalent to the above, is : is the structure of the SimpleLuceneQuery/SimpleLuceneQueryBean and SimpleLuceneCriterion/SimpleLuceneCriterionBean as good as it needs to be? Can it be improved? Is it easy enough to extend it? Is the Interface overkill? If it is desirable, is it properly done etc. etc.

The 'bool' and 'match' fields would probably be candidates for being enumerations, but I never worked out how to do that.

I assume there is already an equivalent to our PersistenceFactory for ORO in Cocoon.

Should QueryFavourites.js be rewritten in Java? Should it be in the style of a DAO?

I guess I need to add a new Block to Cocoon, eek I have never done one !!



Once again, I would like to express my thanks to Antonio for offering to help with this.

regards Jeremy

--------------------------------------------------------

                  If email from this address is not signed
                                IT IS NOT FROM ME

                        Always check the label, folks !!!!!
--------------------------------------------------------



Reply via email to