Skip to main content

Mobile Memories: The MIDP Record Management System

November 16, 2004

{cs.r.title}








Contents
Storing Data
The Record Management System
The RecordStore Class
   Adding and Updating Records
   Retrieving Records
   The enumerateRecords Method
   Working with RecordEnumeration
Scope and Visibility
Conclusion
Resources

Welcome to the third part of my introduction to J2ME. In this installment, I will take a closer look at the persistence package that Duke's Diary (the sample program from the previous article) uses to store its entries.

Storing Data

Just like ordinary desktop-based programs, MIDlets need a facility to store data permanently--that is, to save important information before execution of the program stops. We may distinguish between application data and user data. The latter is most important, as it is data entered or obtained by the user. The entries typed into Duke's Diary can be thought of as user data. The same applies to stock exchange rates obtained over the air, or the current level and score in an action game. If a MIDlet saves its current state (for example, which screen is being displayed, or what preferences have been set by the user), we consider this application data. If you are used to J2SE development, you might assume there is some sort of File or stream-based input/output available, as well as a convenience class like Properties, with its store and load methods.

However, the capabilities of cellphones and other mobile devices are quite limited when compared to desktop systems. Due to such restrictions, there is no concept of an ordinary (hierarchical) filesystem in a MIDP-based environment. Additionally, my distinction between user data and application data is not reflected in J2ME core classes. If you need to store information, you have to use classes and interfaces belonging to the persistence package javax.microedition.rms.

The Record Management System

The Record Management System (RMS) is built around the RecordStoreclass, which is accompanied by helper classes (RecordStore-related exceptions, for example) and interfaces (which may be implemented to ease browsing through the store). It provides a simple record-oriented database. To get an idea of what the term record means, think of addresses or diary- or appointment-like entries. Though each entry contains unique information (for example, the name, address, date of birth, and telephone and fax numbers of exactly one person), its general structure is identical. Each record includes space for the same set of fields. So related information (consisting of the fields I mentioned) is grouped into chunks called records.

MIDlets read and write records through a unique identifier called the recordId. When a new record is added to the record store, it is assigned a new recordId, which stays the same as long as the record store exists. MIDlets may access records in any order, but have to read or write whole records. This implies that random access to the contents of the database is not on a byte level, but rather on a record level. recordIds range from 1 to n, where n is the total number of records in a store.

What information is stored in a record is not important to the RMS. It even does not know about how records are organized. It sees them as arrays of bytes, so the physical layout of records and their size may vary--the MIDlet is responsible for handling this correctly. The only thing important to the RMS is the recordId, which acts as a primary key. Figure 1 illustrates the concept of records in a record store.

Figure 1
Figure 1. The concept of records in a record store

The RecordStore Class

The RecordStore class provides full access to a record store. Its static openRecordStore methods are used to create new stores (if the store identified by the supplied name does not exist) and open existing ones. To the programmer this is completely transparent, as you simply call openRecordStore and work with the store. You just have to make sure your program can handle empty record stores. This is the case if getNumRecords() returns 0.

It is good practice to close a record store using closeRecordStore if the MIDlet does no longer needs to access it, in order to free valuable memory and resources on the mobile device. On the other hand, you should avoid frequent open-close cycles. In such cases it's better to keep the store open. This applies to diary-, address- and calendar-like applications, for example. But if a record store contains infrequently accessed high scores for a game, for instance, you can safely close it after adding a new entry.

Adding and Updating Records

Once a store has been opened, you can fetch existing records and modify and save them, as well as add new data. The latter task is achieved with the addRecord method. To modify an existing record you can call setRecord.

The save method below (which is part of the RecordStoreItem class of Duke's Diary) can add new entries as well as update existing ones. The appropriate operation is determined through recordId, which is an instance variable of RecordStoreItem. save also illustrates how the byte array of a record can be filled. In this example, a record consists of two Strings that are returned by getLabel and getText. They are converted into bytes by calling getByte and then copied into the record. Please note that I have used two for loops because are easy to understand. In real world MIDlets you should, however, use System.arraycopy for efficiency reasons.

private void save() {
  byte [] dateBytes = getLabel().getBytes();
  int dataBytesLength = dateBytes.length;
  byte [] textBytes = getText().getBytes();
  int textBytesLength = textBytes.length;
  byte [] data = new byte [dataBytesLength + textBytesLength];
  int i;
  for (i = 0; i < dataBytesLength; i++)
    data[i] = dateBytes[i];
  for (i = 0; i < textBytesLength; i++)
    data[dataBytesLength + i] = textBytes[i];
  try {
    if (recordId == -1)
recordId = rs.addRecord(data, 0, data.length);
    else
rs.setRecord(recordId, data, 0, data.length);
  } catch (RecordStoreException e) {
  }
}

As addRecord and setRecord are straightforward to use, we will focus on how to retrieve records.

Retrieving Records

An individual record can be fetched from the record store using getRecord. If you are searching for a particular entry, you need to have a look at all records in the store. I have already introduced getNumRecords, so you may be tempted to implement something like this:

byte [] result;
for (int i = 1; i <= rs.getNumRecords(); i++)
  /*
   * put try inside the loop
   * because if one record is not
   * available we want still to
   * try all others
   */
  try {
    if ((result = rs.getRecord(i)) != null) {
      ...
    }
  } catch (RecordStoreException e) {
  }
}

This is not good practice. Each method call returns a newly allocated byte array. If your MIDlet does not explicitly need different arrays, it is better to allocate memory once (which has the additional benefit of increased performance) and reuse the array for subsequent calls to getRecord (there is a variant that takes an array as one of its arguments). An even better approach, however, is to call enumerateRecords.

The enumerateRecords Method
enumerateRecords is used to retrieve a subset of the records in a record store. Its behavior can be influenced in two ways. First, a filter controls which records are retrieved. As we will soon see, the filter is implemented as a method that is called for each record of a store. If the filter method returns true, the corresponding record is included in the result. Second, a comparator specifies how retrieved records are ordered. For each combination of two records, a method is called whose return value determines the order.
The filter and comparator are defined in the RecordComparator and RecordFilter interfaces, respectively.

If neither the filter nor the comparator is specified, all records of a store are retrieved in an unspecified but very efficient manner, so you should use enumerateRecords(null, null, false) instead of the loop discussed above. The two null arguments tell the runtime that neither a filter nor a comparator should be used. The boolean value will be discussed later.

A class that implements both the RecordComparator and RecordFilter interfaces must provide the following methods:

public boolean matches(byte[] candidate) {
  ...
}

public int compare(byte[] rec1,
                   byte[] rec2) {
  ...
}

It is easiest to have your MIDlet main class implement both interfaces because this adds only two methods to your MIDlet, but helps keep it small and efficient. If your application needs to offer several sort criteria (for example, by name, by date, and by category) you can control the behavior through some instance variable that is checked inside of compare, which acts accordingly.

Both methods are passed byte arrays that contain a complete record. It is totally up to your MIDlet to interpret the arrays. Below, you can see how Duke's Diary implements them.

/*
* RecordFilter interface
*/
public boolean matches(byte[] candidate) {
  String label = getDateFromRecord(candidate);
  long date = getDateAsLong(label);
  return isSameDay(today, date);
}

/*
* RecordComparator interface
*/
public int compare(byte[] rec1, byte[] rec2) {
  String date1 = getDateFromRecord(rec1);
  String date2 = getDateFromRecord(rec2);
  long val1 = getDateAsLong(date1);
  long val2 = getDateAsLong(date2);
  if (val1 == val2)
      return RecordComparator.EQUIVALENT;
  else if (val1 < val2)
      return RecordComparator.PRECEDES;
  else
      return RecordComparator.FOLLOWS;
}

The filter (matches) should return true if your application wishes to include the record in the result of enumerateRecords. The comparator (compare) returns one of three values, which are defined in the RecordComparator interface:

  • EQUIVALENT means that in terms of search or sort order, two records are the same.
  • FOLLOWS means that the first parameter follows the second one, which means that in terms of search or sort order, the first is bigger or greater than the second.
  • PRECEDES means that the first parameter precedes the second one, which means that in terms of search or sort order, the first is smaller or lesser than the second.
Working with RecordEnumeration

To get an idea of how enumerateRecords is actually used, we again will have a look at Duke's Diary, which reads all entries for the current date.

try {
    today = getDateAsLong(getDateAsHexString(currentDate));
    RecordEnumeration recEnum = recordStore.enumerateRecords(this, this, false);
    currentEntries = new RecordStoreItem[recEnum.numRecords()];
    for (int i = 0; i < currentEntries.length; i++) {
int recordId = recEnum.nextRecordId();
byte [] result = recordStore.getRecord(recordId);
currentEntries[i] = addNewItem(recordId, result);
    }
    recEnum.destroy();
} catch (RecordStoreException e) {
}
enumerateRecords returns an enumeration that is specified in the RecordEnumeration interface. It contains all records that comply with the specified filter criteria. Cycling through the enumeration takes place in an order controlled by the comparator argument. To obtain the total number of records in this enumeration, you can call numRecords. The recordId of the next record to be retrieved is obtained with nextRecordId. As the enumeration is bidirectional, there is also a previousRecordId method that cycles through the enumeration in the opposite direction. Please note that if there is no previous or next record, an InvalidRecordIDException is thrown. Though you can catch the exception it is best to call hasNextElement or hasPreviousElement.

Once you have finished working with the enumeration, you should call destroy to free internal resources that are no longer used.

The boolean argument keepUpdated, of enumerateRecords, controls whether modifications of the record store are reflected in the enumeration. If you set it to true and, for example, call addRecord, deleteRecord, or setRecord, the enumeration will be rebuilt. Doing so ensures that no InvalidRecordIDExceptions are raised when your MIDlet tries to access a record that is no longer present (maybe because it has been deleted by another MIDlet). However, enabling keepUpdated may decrease the performance of your MIDlet, so you should carefully consider whether you need to keep track of changes.

Scope and Visibility

As there is no ordinary file system in a MIDP environment, you may wonder where record stores are saved and how they can be distinguished. First, the physical location of a record store as well as how it appears in memory or on mass storage media are not exposed to a MIDlet. Second, please remember that although you develop MIDlets, you distribute MIDlet suites. A MIDlet suite must contain at least one MIDlet. If a MIDlet creates a record store, this store belongs to a MIDlet suite rather than a MIDlet. More precisely, the record store is associated to the MIDlet suite to which the MIDlet that created the store belongs. In MIDP 1.0, stores can neither be seen nor shared across MIDlet suites. So the names of record stores (which are case-sensitive and may consist of up to 32 characters) must be unique in a MIDlet suite, but there is no problem if MIDlets of completely unrelated suites create record stores with the same name.

MIDP 2.0 introduced new variants of openRecordStore, and a method called setMode, which provide some influence on which MIDlets can access a record store. RecordStore defines two authorization modes: AUTHMODE_PRIVATE, which maintains compatibility with MIDP 1.0, and AUTHMODE_ANY, which allows any MIDlet on the device to access a record store. Access controls are defined when a record store to be shared is created, and are enforced when such a store is opened. A MIDlet can allow others to access its record store(s) by calling setMode (for existing ones) or by setting an appropriate authmode when calling openRecordStore via the signature:

   public static RecordStore openRecordStore(
        String recordStoreName, boolean createIfNecessary,
        int authmode, boolean writable)

The MIDlet suite that owns a record store can always read, add, and update records. The writable argument specifies whether MIDlets of other suites may modify it. If it is set to false, they can only read records.

If a MIDlet needs to access a record store that belongs to another suite, it opens the store via a different openRecordStore signature:

public static RecordStore openRecordStore(
    String recordStoreName, String vendorName,
    String suiteName)

The target suite is identified by the MIDlet vendor and MIDlet name. These are attributes stored in the Java application descriptor file.

Please keep in mind that AUTHMODE_ANY should be used very carefully. It is important that you take extreme care that no sensitive data is kept in a shared record store.

Conclusion

Despite the limited capabilities of mobile devices, the Record Management System provides a convenient and easy-to-use infrastructure for longterm persistence of user data. In the next part of this series, we will once again turn to the javax.microedition.lcdui package and have a closer look at low-level display output. We will also see how input facilities are handled.

Resources

width="1" height="1" border="0" alt=" " />
Thomas Kunneth works as a software architect at the German authorities, specializing in Java-based rich clients.
Related Topics >> GUI   |   Mobility   |   Programming   |