Document last updated 11 April 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ñolThis 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.
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 Niggle API's for data persistence that keep the configuration details well encapsulated in external XML files.
The Niggle API's for persistent data that we introduce
are in the package com.revusky.niggle.data
. There are 3 main
constructs that this example introduces:
com.revusky.niggle.data.MutableDataSource
, com.revusky.niggle.data.Record
,
and com.revusky.niggle.data.DataRegistry
.
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. The third object mentioned above, the data registry,
encapsulates the metadata in the system. It allows
us to look up the available data sources by name. It also allows us to
get a "virgin" instance of a record of a given type so that we can fill it
with data. Note that this metadata is specified externally in the
accompanying XML files, recorddefs.xml and datasources.xml. Thus, the
com.revusky.niggle.data.DataRegistry
object is really the bridge
between the java coding level and that external XML specification.
Let's get our bearings by looking at the java code and seeing where these new API's mentioned above are used.
In the minirolo example, the retrieval of records is really contained in the 4 methods
at the bottom of the MiniRoloServletInteraction class,
getData()
, getAllEntries()
, getEntry()
and newEntry()
.
Take a look at the getData
method. It is only two lines.
The first line retrieves the singleton instance of our data registry object.
This allows us to look up our data source by name. Note
that the lookup name of "rolodex_data" is arbitrary enough. It so happens
that this is the name of the data source specified in the external file, datasources.xml.
So, getData()
fishes out a handle to the data source we are using
in this servlet.
The getAllEntries()
method returns all the records in the data source
in a java.util.List
object. It does this by calling the select()
method of our data source object with a null argument. The select()
method can take an argument of type com.revusky.niggle.data.RecordFilter
which
implies a subset of records to retrieve. But if null is passed in as the argument, as is the
case here, the list returned contains all of the records in the data source.
The getEntry()
method is a bit more complicated.
It takes as a given that the primary key of the record we
are working on is embedded in the servlet request via unique_id=
.
If there is no entry available this way, it returns a new "virgin entry" by
calling newEntry()
. What this last method does
is that it invokes a method on the DataRegistry object
that vends an exemplar of the
record type we are interested in by name, in this case,
the name being "rolodex_entry". Note that
this works because the metadata for "rolodex_entry" is
specified in the recorddefs.xml file!
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 Niggle's 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 niggle records. It is important
to note here that Niggle 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 and there is also an execDelete()
dispatch
handler.
Let's look at the execDelete()
method first because it's simpler.
In the first line, the code extracts the unique_id=
parameter from
the servlet request. It then fishes out the corresponding record. The next line
invokes the delete()
method on our data source, which
takes the record's primary key as an argument. After that, page template
variable is defined. This corresponds to "ack.nhtml" which is used to acknowledge
that an operation has occured. We set the boolean variable deleted
to indicate that the operation that ocurred was a deletion.
The execProcess()
method, which processes the
editing page, is somewhat more complex. One reason is that it must distinguish between
insertion of a new record and the modification of an existing one. As you can
see, it retrieves the record to operate on via getEntry()
.
Now, getEntry()
will either return a new "virgin"
record (if no unique_id= was embedded in the request) or the existing record
to modify. The way it distinguishes between the two is by the isImmutable()
call. You see, a newly created niggle 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. That is why, with an existing record, we
have to make the call to getMutableCopy
prior to filling in the
fields from the servlet request.
The DataUtil.fillRecordFromServletRequest()
method is a
convenience method that uses the record's metadata (which it fishes out behind
the scenes) to iterate over the record's fields and fill them in based on the parameters
in a 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.
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.niggle.data.DataException
.
The recover() method gives us a chance to handle such conditions gracefully.
Now we will turn our attention to the XML configuration files. Let's start by looking at datasources.xml. This may be more or less intimidating to you, depending on the extent to which you have mucked with XML before this. There is not much reason to be intimidated, since really, XML is being used here to provide a human-readable external configuration format, in this case, so that we can keep the details of our persistence layer completely separate logically from the application-level code.
This is really the minimal example of such a file, since it defines exactly one DataSource, to which we assign the lookup-up name of "rolodex_data".
The CLASS attribute is set to that of the default flat file implementation:
com.revusky.niggle.data.inmemory.InMemoryRecordSet
. This is an implementation of the
MutableDataSource
interface that simply stores records in a flat text file.
This is very useful for development. And actually, even for deployment, this data source
implementation can be used for projects up to a moderate scale. At a later point,
one could convert to using a DATASOURCE CLASS that is wraps the connection to an industrial-strength
RDBMS. And you could achieve this basically by changing a line in your XML.
In any case, as the the nomenclature suggests, the records in an InMemoryRecordSet
are kept in memory. The persistence scheme used is a flat text file. The two properties defined, STORE and
PERSIST_FREQUENCY, configure where that flat text file is located, and the frequency with which it is
rewritten from scratch. If you configured a JDBC data source here, you would likely have to set
some other parameters, but we will leave those details for later. In any case, as you see, the details of
your data persistence mechanism are completely transparent to your application-level java code
as well as your presentation layer, the page templates.
Now, it's time to look at the other piece of the puzzle, the record metadata. If you look at recorddefs.xml, you will see that it defines exactly one record type, called "rolodex_entry".
In any case, the first field in our record definition is the field "unique_id", which is also the primary key. Also, it so happens that our DataSource class will automatically set it to a random unique key every time a record is stored.
The other fields defined in the rolodex_entry record are all strings. Some are required. Some are not. Also, certain of the fields have defined normalizations which provides some extra information to the niggle system about how to clean up the data when it is read in from the user interface. In any case, it will probably become apparent what a field being "required" or a field being "capitalized" means, simply by playing with the sample servlet.
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.
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.
At this point, this has only been tested using MySQL. I see no reason it would not work with an arbitrary RDBMS, as long as it has a JDBC driver. Still, I would be very interested in receiving confirmation of this. If you've managed to get the example working using some other RDBMS, or you've run into problems, please do write me to share your experience!
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.