Document last updated 28 May 2001. We are very interested in feedback that would make these materials better. Feel free to write the author, Jonathan Revusky.

Este documento en español
Ce document en français

Dissecting the mini-rolodex example

This example assumes that you have successfully built and run the mini-rolodex servlet, as described in the instructions here. Once you've successfully gone through those mechanical steps, you will probably want some help in dissecting the elements of this example.

Introducing the Oreo Data API

The mini-rolodex example represents a huge leap forward as compared to the guestbook. This is because this example actually stores and retrieves its application data from a persist store. Perhaps surprisingly, the example does not require that much more java code than the guestbook did. The reason is that it leverages the Oreo API's for data persistence and this allows us to keep the configuration details well encapsulated in external XML files.

The Oreo API's for persistent data that we introduce are in the package com.revusky.oreo. There are 2 main constructs that this example introduces: com.revusky.oreo.MutableDataSource and com.revusky.oreo.Record.

Though there are various possible nomenclatures from computer science and database theory, hopefully, most application programmers will be comfortable with the nomenclature that we have settled on: a mutable data source is an object that allows us to store, retrieve, and modify records. In turn, a record is basically the same thing conceptually as a row in an RDBMS table. Sometimes, this is alternatively referred to as a tuple or an entity.

The Oreo Record

Like a row in a database table, an oreo record simply represents a set of key/value pairs that conform to a certain metadata schema -- i.e. a description of the names of the keys and restrictions on the values associated with them. This is what we mean when we refer to a record's fields. All the records of a given type have the same metadata -- i.e. the same field layout. Furthermore, it is assumed that for every record type, exactly one of the fields is the primary key field. The combination of a record's type and the value of its primary key must serve to uniquely identify a record for storage/retrieval in a DataSource object.

If you have not done so already, have a look at the recorddefs.xml file. Just how scary this is depends on the extent to which you have worked with XML before. There is no reason to be intimidated. We're just using XML as a human-readable configuration file format. Every XML file is a singly-rooted hierarchy of elements. The root element in this file is RECORDDEFS, which contains one or more RECORD elements. This being still a minimal example, it only defines one record type, of type rolodex_entry. The PRIMARY_KEY is the unique_id field. That field, the first field defined within the RECORD element, is defined as a numerical field, of type INTEGER, and it is non-negative, since the MINIMUM property is set to zero. The next two fields, first_name and last_name are string fields. Note that, in addition to being specified as REQUIRED, they also have a couple of NORMALIZE directives associated with them. These can be useful things to specify, since they specify certain typical cleanups to perform when reading the field value from a UI. For example, you don't really want to treat the string "Smith " as something different from just "Smith" or even " SMITH". The two normalize statements allow us to say that we want to treat all of the above strings as being the same, which from a human point of view (as opposed to the computer point of view) they most certainly are.

The next field, email is also required, and it is of type email. This is a convenience that the library provides out-of-the-box that allows you to check whether an email is well-formed. The next fields are optional. I think that says everything we need to know about our record definition. Now, let's take a look at the data source configuration.

Oreo Data Sources

If you look at the datasources.xml file, you will see that it is pretty brief. There are only 2 configuration properties: DATA_PATH and COMPACT_FREQUENCY. The DATA_PATH simply specifies a file where the data should be stored. This should be read/writeable by the process on your system. The COMPACT_FREQUENCY is a rather technical parameter which is optional, since if it is not specified, a default value will be used. It says how frequently a snapshot of the persistent store should be written again in a compact (or fairly compact) format. With a value of 100 specified, this means that 99 out of every hundred writes to the disk simply append information to the file. The 100th writes the database from scratch in more compact form.

Here is something to bear in mind as we move forward to look at the java code. In the 2 XML configuration files, we defined one record type and one data source. The record type was named "rolodex_entry" and the data source was named "rolodex_data". These are the names that we use to get our grubby hands on them in the java code!

Doing Stuff with Records

Well, it's all very well to specify things in a nice well-defined format, but that doesn't do anything in and of itself. It's time now to look at where this is used in the actual java code.

Have a look at MiniRoloServletInteraction.java. The minirolo servlet is really pretty simple. Its functionality can be characterized by 4 basic actions:

  1. Display all the records. (This is the default action.)
  2. Delete a record. (This is the delete action.)
  3. Give the user a page with a form for editing a record. (This is the edit action.
  4. Process the results of the form. (This is the process action.

The above actions correspond to the methods execDefault(), execDelete(), execEdit(), and execProcess()respectively.

The Default Action: Displaying all the Entries

Let's start by looking at the execDefault() method. This is the method that provides the default view, which is simply a list of all of our entries. It is surprisingly brief. What it does is that it starts off by getting a handle to the one data source we defined. Then, in the next line, it invokes the select() method on the data source object with a null argument. Note that the select() argument generally takes a parameter of type com.revusky.oreo.RecordFilter which implies a subset of records to retrieve from the data source. But if, as here, a null is passed in as the argument, the list returned simply contains all the records in the data source object.

The next line will be familiar if you went through the previous tutorials. We simply fish out the page template we are going to use to display our entries. Then the next line exposes the list at one go. If you want, you can look at the entries.nhtml file to see where all the magic happens.

The Delete Action

Now, let's look at how the delete action is handled in execDelete(). It starts off the same way that the execDefault() method mentioned above does. It gets a handle to our one data source, which has the lookup name of "rolodex_data". Now, the delete action refers to deleting one specific entry in the data source. Remember how we said above that the combination of type and primary key would uniquely identify a record in a data source. Well, the delete assumes that the unique_id is embedded as a parameter in the servlet request. Since both the delete and edit actions do this, geting the unique ID has been broken out into its own little method, called getEntryID(), which returns an Integer (or null) based on the unique_id parameter in the servlet request.

Now that we have a unique lookup id and a data source, we can get the record in question using the get() method of the MutableDataSource object. Assuming that what we get back is non-null, we then invoke the delete() method on the record.

Now it is time to output an acknowledgement of the operation to the client. We fish out the ack.nhtml template and expose some information. We expose a variable called operation that indicates that the operation performed was a deletion. And we expose the record that was removed from the data source, in case the client is to be given a final look on the acknowledgement page. (Note that this kind of pattern applies to all kinds of typical sorts of e-commerce web sites.)

The Edit Action

The code for the edit action is not very different really from the code of the last example. The difference is that we use this same action for what are really 2 cases:

It is only in the second of these 2 cases that we need a primary key lookup ID. Basically, what this app does is that it looks for a primary key in the servlet request parameters (just like the delete action handler did) and if it finds one, it asumes that the user wants to modify the corresponding entry. If there is no unique_id= parameter in the servlet request, it is assumed that the user wants to insert a new one.

Feel free to look at the edit.nhtml page template to see how this all fits together.

The Process Action

Though it's still pretty brief, execProcess() is the most complicated action handler. The main reason is that it distinguishes the two cases, of whether someone is inserting a new entry or modifying an existing one. Again, this is based on whether the servlet request has a "unique_id=" parameter embedded in it. The first case dealt with in the code is where there is no unique_id parameter, so this is a new entry.

There are a couple of things to take note of in this method. The first you will note is that there is a convenient fillInFields() method that rather magically fills in a record's field values on the basis of the parameters in the servlet request. It does that by using the record's metadata to iterate over the record's fields and fill them in based on the parameters in the servlet request. Once we have done that, we call insert() or update() on the data source depending on whether this is a new entry or the modification of an existing one. Finally, after we have performed our data modifications, we fish out the template page to acknowledge that the operation was performed and then we expose the record, which is interpreted as a hash variable on the template layer.

A basic difference between the first and second case dealt with in the handler is the call to getMutableCopy in the case where we are updating an existing record. You see, a newly created Oreo record is in a mutable state. When it is placed in a data source container, it is made immutable. To modify that record, we actually create a mutable clone, perform the modifications and then perform the replacement. You may (or may not) realize that this seemingly complicated disposition is all about guaranteeing thread safety in the framework. We will develop these ideas at a later point. At this stage, what you need to understand is that modifying an existing record is fundamentally different from inserting a new one. The code in this method is the first (but certainly not the last) time you will encounter this.

Recovery from Errors

The final novelty in this example is the recover() method. This is a hook that gives us a chance to handle errors. For example, if, in our external metadata configuration, we define a field (last name for example) as required and the user does not fill it in, the persistence engine will throw an instance of com.revusky.oreo.DataException. The recover() method gives us a chance to handle such conditions gracefully.

Summing up

It is interesting to compare and contrast this servlet example with the previous example, the guestbook. For example, both the guestbook and this example contain an execEntries() method and in fact, they are almost identical. The difference is that the minirolo example interacts with the Oreo data persistence layer to get the list of entries to expose to the page template mechanism. In the guestbook example, the elements of the list were hashtables. Here, they are Oreo records. It is important to note here that Oreo records can be exposed to the template mechanism in exactly the same way that a plain hashtable (or any other object that implements java.util.Map) can be. This feature is also utilized in the execEdit() method.

Another way in which the mini-rolodex is more complex than the guestbook is that, where the guestbook only permitted insertion, this example also allows modification and deletion. Thus, the execProcess() method is more complex, since it must handle those two cases differently.

This example contains pretty much all of the conceptual elements of a more complex Niggle-based web application. If you manage to become comfortable with all of these elements, you will have overcome the main hurdles involved in learning the Niggle framework. There are more features than what you see here, and more will be added over time, since Niggle is an ongoing project. But the things you learn from hereon will be more incremental in nature.

Addendum: Using a Relational Database instead of flat files

Note that the file datasources2.xml contains a substitute data source config file that uses an external RDBMS for persistence. To use this, you will definitely have to adjust some of the parameters.

Since this example is quite simple, it should work with an arbitrary RDBMS as long as you adjust the JDBC_URL and JDBC_DRIVER_CLASS parameters. I see no reason it would not work with an arbitrary RDBMS, as long as it has a JDBC driver.

In any case, a good modus operandi will be to get your basic application logic working against the flat files, since it requires no external configuration and then switch to the external RDBMS afterwards if it proves necessary.