Welcome back to my series on J2ME development with MIDP. If you’ve been following this series from the beginning, you’ll recall that last time, we modified the ExpensesApp MIDlet suite to allow the editing of existing items and the addition of new ones. Any new expense items added using the application weren’t saved across runs, which made ExpensesApp only slightly less useful than a football bat (American or otherwise).
In this installment, we’ll remedy the situation by creating a record store for the ExpensesApp MIDlet suite using the MIDP’s Record Management System (RMS). Then we’ll hook ExpensesApp up to this record store, which will finally make the app, theoretically at least, useful.
Parts 1 and 2
If you've joined our series in progress, be sure to check out the first two installments:
What's a record store?
The MIDP specification requires that a platform provide some form of persistent storage via nonvolatile memory. The RMS manages these record stores, which are simply flat files containing binary data. Each piece of data in a record store is referred to as a record and has an associated numeric record ID that is unique to that record store. Each record store has a name that must be unique within the MIDlet suite that created it, and MIDlets can access only record stores created by themselves or other MIDlets in the same suite. When a MIDlet suite is removed from a device, all its associated record stores are deleted with it.
The javax.microedition.rms package contains a RecordStore class that provides rudimentary access to data in a record store. This package also contains additional classes that allow you to sort and search for records in a record store. In this article, I’ll concentrate on RecordStore for simplicity’s sake and cover the other classes in the future.
Working with RecordStore
The process of working with RecordStore is fairly straightforward. To open an existing record store, you use the static RecordStore.openRecordStore method, which returns a RecordStore instance for a named record store. This method can also create a new record store (see the createIfNecessary parameter) if the name you specify doesn’t already exist. You retrieve individual records from a store by their unique record IDs using the getRecord method, while you add and update records with the addRecord and setRecord methods, respectively. When you are finished working with a record store, be sure to close it with closeRecordStore.
A quick note on record IDs
In Sun’s reference implementation, the record ID for a given record is the same as its insertion order: The first record added to a record store will have an ID of 1, the second’s will be 2, and so on. While that sounds convenient, don’t make the mistake of assuming it will always be the case. The MIDP’s specification requires only that each record have an ID—how that ID is created is left up to the platform implementer.
Most methods of the RecordStore class throw one or more exceptions of type RecordStoreException. These exception subclasses and their meanings appear below:
- · InvalidRecordIDException is thrown when a method call references a record ID that does not exist (when reading or updating a record) or would be invalid (when adding a new record).
- · RecordStoreFullException is thrown to indicate that the RMS’s storage is full. Note that this exception can be thrown from unexpected methods. For example, OpenRecordStore, which opens a named record store, can throw RecordStoreFullException, even though it doesn’t appear to update the record store itself.
- · RecordStoreNotFoundException is thrown when a method call references a record store that does not exist. The OpenRecordStore method can create a record store if it doesn’t exist. This exception can also be thrown from a strange place: CloseRecordStore.
- · RecordStoreNotOpenException is thrown if you try to access a record store without first opening it using OpenRecordStore.
Finally, because they deal with binary data, the record access methods (getRecord, addRecord, and setRecord) all handle record data as byte arrays. You’ll find, unless you’re simply masochistic, the use of the java.io package’s stream classes, such as ByteArrayInputStream, ByteArrayOutputStream, DataInputStream, and DataOutputStream, to be incredibly useful.
Digging into the code
You can download the source code for the latest version of ExpensesApp here. I originally intended to modify only the ExpenseInfo class while working on this article, but RecordStore is implemented in a quirky way that makes doing so incredibly complicated. (It would have required several nested try blocks, among other things.) As a result, some of my changes spread to the Expenses and DetailForm classes to keep things simple.
In fact, most of the RMS classes have similar quirks, which taken together can make for a frustrating development experience. Here are just a few of the problems I ran across: Similar methods in different classes have dissimilar names; some methods are deceptively named; and many methods throw exceptions in response to situations that you as the programmer can’t do much about. You’ll find having a copy of MIDP’s JavaDoc on hand, or at least an editor with method prompting features, to be invaluable if you do much work with J2ME’s RMS classes.
Looking at the code in Listing A, you’ll see that the static ExpenseInfo.LoadExpenses method has been modified to read data from a RecordStore object. After opening or creating the record store, I loop through all the records contained in the store, extracting the data for each into a byte array, create a new ExpenseItem instance for each, and return the whole set of items in a Vector. ExpenseInfo.LoadExpenses also now throws RecordStoreException exceptions, a change made imperative by the fact that closeRecordStore itself throws exceptions, so a try block needed to be added to the Expenses class constructor as well. In the “real world,” you’d probably want to do things that way anyway, since you’ll want to propagate any RMS errors into your UI.
I also updated the ExpenseInfo.save and ExpenseInfo.delete methods to work with the RMS API. These methods can be found in Listing B. Both methods check to see whether a record exists for the current instance by checking for a nonzero record ID (the private ExpenseID variable). If save is invoked on a new, unsaved expense item, addRecord is called to add a new record to the record store. If the record already exists, the method calls setRecord to update the appropriate record. The delete method works similarly, doing nothing if its instance has no record ID (because it has yet to be saved), and invokes deleteRecord to remove the appropriate record if ExpenseID is nonzero.
There's still more work to do
We’re making strides, but ExpensesApp isn’t quite done yet. Its current implementation includes a handful of problems:
- · The delete functionality is bugged: Deleting a record from the middle of the record store file will cause LoadExpenses to throw an InvalidRecordIDException. For this reason, the cmDelete Command on lsMain isn’t enabled.
- · Expense entries are shown in the order they were entered in lsMain. It would be more useful if the entries could be sorted by date.
Can you find the bug?
Consider this a practical exercise. Uncomment the marked lines in the Expense class’s constructor to enable the delete functionality. Run ExpensesApp and add a few new expense items, then delete the second one you entered. The next time you start ExpensesApp, you should catch an exception. If you track down the cause of this bug and e-mail me an explanation for it, I’ll mention you in the next article in this series.
In my next Exploring J2ME article, I’ll show you how to use the RecordEnumeration class to solve these two problems. Until next time, happy coding.