Give Your Mobile Workforce Offline Muscle with the MIDP Record Management System

distinguishing factor of J2ME over browser-based approaches is the ability to store data on the device. This allows networked applications to be useful even in the absence of a network connection because data can be stored locally on the device for off-line browsing. Additionally, a user can modify data while out of coverage and the application can store the changes and transmit them to a server after the device regains a network connection. In some cases an application may be able to handle the device-server network data synchronization activities in the background, allowing the user to move in and out of network coverage while remaining productive. In fact, the user may not even ever be aware that network coverage was lost.

This article focuses on the Record Management System APIs (RMS) available in the Mobile Information Device Profile (MIDP). The RMS can be used to store data directly on a device for offline browsing and storing other application-related data. These APIs are found in the package javax.microedition.rms.

Can I Get a Byte?
At the heart of the RMS is something called a RecordStore, which is a container that holds a set of related records. RecordStores are referenced by name and each record is an array of bytes with an integer ID that marks its location within the RecordStore. Figure 1 shows the basic layout of the RecordStore architecture.

Figure 1. Inside the RecordStore: A MIDP RecordStore has a name and is composed of 1 to N byte array records.

The RecordStore class provides a means for performing standard CRUD (Create, Read, Update, and Delete) operations as well as some methods for sorting and filtering. As records are added each record is automatically assigned the next available record ID. In most cases, record IDs are assigned sequentially, starting at 1. However, as records are added and deleted throughout the life of a RecordStore, the next available ID is not always guaranteed to be the next number in a sequence. The lesson here is to not make assumptions as to what record ID is being assigned to a particular record.

Using the RecordStore
In order to use a RecordStore it first must be opened. If the RecordStore does not exist, it must be created as well. The following line of code can do both of these operations.

RecordStore rs =     RecordStore.openRecordStore("MyStoreName", true);

The first parameter indicates the name of the store to open. The second parameter tells the method to create the store if one by the specified name is not found. A reference to the open RecordStore is returned.

Once the RecordStore is open it can be used to store data. The following lines of code add a record, retrieve the record, update it, and then save it again before closing the RecordStore.

byte[] dataBytes = "Hello World".getBytes();int key = rs.addRecord(  dataBytes, 0, dataBytes.length);byte[] readBytes = rs.getRecord(key);String updatedRec = new String(dataBytes) + " two";byte[] updatedBytes = updatedRec.getBytes();rs.setRecord(  key, updatedBytes, 0, updatedBytes.length);rs.closeRecordStore();

The last operation in this example is a call to close the RecordStore. This is critical in order to make optimal use of the RMS resources. On an actual device, there is usually a limit to the number of RecordStores that can be open at the same time.

Note that when dealing with records that all data must be converted to a byte array format. This is the data structure for a RecordStore. When data is read from a RecordStore it is always returned as a byte array and usually must be converted to something more useful in order to work with it.

These are the basic operations needed to work with RecordStore. However, in the examples shown so far, the index to the RecordStore record was always known. In most applications a RecordStore will first need to be queried to find records containing the correct data. The next section discusses the RMS architecture and how this can be accomplished.

Getting to Know the RMS Package
The RMS package contains one class, RecordStore, and a number of interfaces that help applications use the RecordStore. Table 1 lists these interfaces and provides a brief description of how each interface is used.

Table 1. The classes and interfaces supported by the RMS package.

RecordComparator Interface that provides sorting criteria to a RecordStore query. This is analogous to the order by clause of a SQL statement.
RecordFilter Interface that provides filtering criteria to a RecordStore query. This is analogous to the where clause of a SQL statement.
RecordEnumeration Interface that provides a means of iterating over a result set returned by a RecordStore query (calls to enumerateRecords()).
RecordListener Interface used to provide a call-back method to notify listeners when data contained by the RecordStore is changed or new records are added.
RecordStore Class that represents a record store and provides methods for interacting with records in the store.

The next few sections discuss each of these classes in more detail.

Sorting Elements in a RecordStore
The RecordComparator interface provides a way to specify sort criteria specific to the content within a RecordStore. There are two steps to implementing a sorted query: 1) implement the RecordComparator interface and 2) query the RecordStore using a call to enumerateRecords(), passing an instance of the class implementing RecordComparator as the sort criteria.

The RecordComparator interface supports one method named compare(byte[], byte[]). The two-byte array parameters represent records in the RecordStore. The job of the RecordComparator is to determine which record should come first in the ordering of the result set. How this is determined is the responsibility of the class implementing the compare method of RecordComparator. A result of EQUIVALENT, PRECEDES, or FOLLOWS is returned indicating if the two records should be treated equal, that the first record should remain first, or that the second record should be placed ahead of the first record. (Note: the first record is the record passed in the first parameter position.)

The following code illustrates how a RecordComparator could be implemented to sort a list of contact names by the first name. In this example each record contains the first name, last name, and phone number of a person. Before the compare can take place, the first names must be parsed from each record.

public int compare(byte[] rec1, byte[] rec2){  //convert from a byte[] to a String  String s = new String(rec1);  //parse out the first name  int idx = s.indexOf("|");  String firstName1 = s.substring(0, idx);  s = new String(rec2);  idx = s.indexOf("|");  String firstName2 = s.substring(0, idx);		  //determine the relative order of the two records  if (firstName1.compareTo(firstName2) == 0){    return RecordComparator.EQUIVALENT;  } else if (firstName1.compareTo(firstName2) > 0)	{    return RecordComparator.FOLLOWS;  }	else{    return RecordComparator.PRECEDES;  }}

Once the first names are parsed, the strings can be compared using the String.compareTo() method to figure out in which order the records should be placed. The query is issued to the RecordStore method enumerateRecords() as shown below. The RecordStore must first be opened before calling this method.

RecordEnumeration e =   rs.enumerateRecords(null, new NameComparator(), false);

Figure 2 shows the results of this query using sample data.

Figure 2. Calling All Contacts: Results from a query of a contact RecordStore using sample data.

The boolean parameter passed into the enumerateRecords() method is a flag: setting this flag to true will cause the internal index of the RecordEnumeration to be rebuilt if any records are added or deleted. Note, however, that you may incur a performance penalty for passing true for this parameter. This issue is discussed in detail later in the article.

Filtering Elements in a RecordStore
Now suppose a user wants to search for specific names in the contact database. This can be accomplished by calling enumerateRecords() and passing a class implementing the RecordFilter interface. RecordFilter requires the method matches(byte[]) to be implemented. The byte array parameter is the record the filter needs to match against. A boolean value of true is returned if the record should be included in the enumerateRecords() result set. A value of false is returned to exclude the record from the result set.

The following code implements a search for names starting with a given String.

class NameFilter implements RecordFilter {  private String criteria;  public NameFilter(String criteria){    this.criteria = criteria;  }  public boolean matches(byte[] candidate){    //if no criteria present, include all records    if ((criteria == null) || (criteria.length()==0))	{      return true;    }    //parse the first name    String s = new String(candidate);    int idx = s.indexOf("|");    String firstName = s.substring(0, idx);	    return s.firstName.startsWith(criteria);	  }}

In this example, a criteria String to match against must be supplied. This is done when the enumerateRecords() method is called. The following code queries for first names beginning with “B.”

RecordEnumeration e = rs.enumerateRecords(  new NameFilter("B"), null, false);

The results of the query are shown in Figure 3, using the same sample data as in the previous example.

Figure 3. Do B: Here is a call to enumerateRecords(), which passes a filter to find only names beginning with the letter “B.”

Of course there will often be cases where an application needs to perform both a sort and a filter in the same query. This is no problem. The following applies to both the NameFilter and the NameComparator to a query.

RecordEnumeration e = rs.enumerateRecords(  new NameFilter("B"), new NameComparator(), false);

Once a query result set is obtained, an application can use the result set to display information to the user, send information on a network connection and so forth.

Iterating Query Results
Support for iterating over query results lies within the RecordEnumeration interface. An object supporting this interface is returned by calls to RecordStore.enumerateRecords().

Consistent with Java iterators, RecordEnumeration can test to see if there is a next record in a sequence with a call to hasNextElement(). If this method returns false, the end of the result set has been reached. A call to nextRecord() will return a byte array of the next record in the sequence. There is also a method nextRecordId() that returns only the ID of the next record, rather than the entire record.

While iterating a RecordEnumeration, it is quite possible that another thread might change a record in the result set. In cases where a call to nextRecord() results in an attempt to access a deleted record, an exception is thrown. To avoid this problem, RecordEnumerations can set a ‘keep updated’ flag to be notified of when additions and deletions take place on the underlying RecordStore. This flag can be set as part of the call to RecordStore.enumerateRecords() (as discussed previously) or by calling keepUpdated(true) on the RecordEnumeration after the query has been obtained. Special care should be taken when using this flag, however. Because the RecordEnumeration must rebuild its index whenever an add or delete takes place on the underlying RecordStore, you could incur a performance penalty.

Listening for RecordStore Changes
In the last section I discussed a way for RecordEnumerations to stay synchronized with the underlying data should records be added or deleted. The RecordEnumeration uses an event listener to support this feature. Other areas of an application can use the same technique by implementing the RecordListener interface and adding it to a specific RecordStore by calling addRecordListener(). Notifications will be sent to a listener whenever a record is added, deleted, or changed. A simple example of a RecordListener is shown below. An instance of this class must be placed on an open RecordStore by calling addRecordListener().

class ChangeNotifier implements RecordListener{  public void recordAdded(RecordStore recordStore,               int recordId){    try {      System.out.println(      "Added a record to "+recordStore.getName() +       "id="+recordId);    } catch (RecordStoreNotOpenException x)	{      x.printStackTrace();    }  }  public void recordChanged(RecordStore recordStore,               int recordId){    try {      System.out.println("Changed a record in "      +recordStore.getName() + " id="+recordId);    } catch (RecordStoreNotOpenException x)	{      x.printStackTrace();    }  }  public void recordDeleted(RecordStore recordStore,          int recordId){    try {      System.out.println("Deleted record from "+      recordStore.getName() + " id="+recordId);    } catch (RecordStoreNotOpenException x){      x.printStackTrace();    }  }}

Making RecordStores Available to Other Applications
As of MIDP 2.0, RecordStores have the ability to publish RecordStore access to other MIDlet suites. This can be an advantage for sharing data between applications. A RecordStore is made available to other MIDlet suites on the device by calling setMode() on an open RecordStore.

The setMode() method takes two parameters. The first parameter must be either AUTHMODE_ANY, which grants access to any application, or AUTHMODE_PRIVATE, which allows only the owning MIDlet suite (the suite that created the RecordStore) to access RecordStore. The second parameter is a boolean that indicates if applications can have write access to the RecordStore. This allows a MIDlet suite to provide other applications read-only access. The owning MIDlet suite always has write access regardless of the AUTHMODE setting.

What’s in Store?
In addition to providing ways to manage data within a RecordStore, the RecordStore class offers some methods to find out what RecordStores are available, how much storage space is available, and so forth. A call to the static method RecordStore.listRecordStores() returns an array of String containing all the names of the RecordStores available to the MIDlet. Other useful instance methods are listed in Table 2.

Table 2. Some Instance Methods for Discovering Information about the RecordStore.

getSize() Returns the size in bytes that a RecordStore currently occupies
getSizeAvailable() Returns the amount of storage, in bytes, that a particular RecordStore could consume.
getVersion() Returns a version of the RecordStore. The version is incremented each time the state of the RecordStore is modified by addRecord(), deleteRecord() or setRecord().
getLastModified() Returns the time and date (in long format) of the last time the RecordStore was modified by addRecord(), deleteRecord() or setRecord().

The RMS provides a simple and clean API for storing data locally on a device. Mechanisms for sorting and filtering data are provided as well as a means for monitoring changes that other threads may be making to a particular RecordStore. As of MIDP 2.0, applications can now share RecordStore data. This feature allows applications to cooperate through the data layer to exchange information.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Overview

Recent Articles: