The RMS APIs

The RMS APIs are in the MIDP package javax.microedition.rms. The central class in the package is the RecordStore class, which represents an RMS record store. The RecordStore class is used to perform all the operations on the record store. The operations allow the developer to

  • Create, open, close, or delete a record store.

  • Add a new record, delete a record, and iterate over the records. Filters and comparators can be set up so that the records are sorted and filtered if as desired.

  • Obtain information about the record store, such as the number of records, its size, the amount of memory remaining for the record store, its name, the time it was last modified, and the number of times it has been modified (i.e., its version).

  • Set up a listener on a record so that a notification is received when the record is modified.

To create and open a new record store, we use the static method openRecordStore on a RecordStore object:

store = RecordStore.openRecordStore(recordStoreName, true);

The second parameter of openRecordStore is a Boolean flag to indicate whether we want to be notified if the record store does not already exist. If the flag is true, openRecordStore will create a new record store if it cannot find one to open, and open it, returning a reference to the object representing the new store. Note that if the record store is being used by another MIDlet in the same suite, a reference to the open record store will be returned.

If we just wanted to open an existing record store, we could use false for this parameter. In this case, a RecordStoreNotFoundException exception would be thrown if the record store did not exist.

To obtain some information about the record store, there are some methods on the RecordStore class that can be used:

StringBuffer result = new StringBuffer();
result.append("Name: " + store.getName() + "\n");
result.append("Records: " + store.getNumRecords() + '\n');
result.append("Store size: " + store.getSize() + " bytes\n");
result.append("Bytes available: " + store.getSizeAvailable() + " bytes\n");
result.append("Version: " + store.getVersion() + "\n");

To close the record store, we simply call the closeRecordStore method on the RecordStore object:

store.closeRecordStore();

Note that every openRecordStore method call needs a corresponding closeRecordStore. Think of it as a counter that is incremented on an openRecordStore and decremented on a closeRecordStore. The record store is not actually closed until the counter is zero again.

To remove a record store completely, we use deleteRecordStore:

RecordStore.deleteRecordStore(recordStoreName);

Because a record store can be shared by a number of MIDlets in a suite, it may be possible that another MIDlet is accessing the record store we are trying to delete. In that case, the deleteRecordStore method will throw a RecordStoreException.

Information is stored in the record store in records, and each record is an array of bytes. The methods to save to and retrieve from a record store all represent the record as an array of bytes. At first glance, having to store information as a byte array may appear to be a cumbersome mechanism. However, with the use of ByteArrayOutputStream and ByteArrayInputStream, and the corresponding DataOutputStream and DataInputStream, it is not as onerous as you might think. Say, for example, that you are writing a movie database application for ordering videos or DVDs over the Internet. The basic object you might want to store in a record store could be along the lines of the following:

package com.javaonpdas.persistence.rms;

import java.io.*;

public class Movie
  public String title;
  public String actors;
  public long yearReleased;

  public Movie() {
  }

  public Movie(String title, String actors, long yearReleased) {
    this.title = title;
    this.actors = actors;
    this.yearReleased = yearReleased;
  }
    public String toString() {
        StringBuffer result = new StringBuffer(title);
        result.append(", released ");
        result.append(yearReleased);
        result.append(", starring ");
        result.append(actors);
        return result.toString();
    }
}

The MID profile does not define a serialization scheme. As we will see in Chapter 7, "Networking," it is possible to define a simple serialization scheme for sending objects over the network, as well as storing objects in record stores.

In our simple serialization scheme, we define an interface that has two methods:

package com.javaonpdas.persistence.rms;

import java.io.*;

public abstract interface Serializable {
  public void writeObject(DataOutputStream dos) throws IOException;
  public void readObject(DataInputStream dis) throws IOException;
}

Objects implementing this interface must define an object-specific way to save the state of the object to a DataOutputStream, and to retrieve it (and implements serializable to the class definition) from a DataInputStream. So, we need to add two methods to our Movie class:

public void writeObject(DataOutputStream dos) throws IOException {
  dos.writeUTF(title);
  dos.writeUTF(actors);
  dos.writeLong(yearReleased);
}

public void readObject(DataInputStream dis) throws IOException {
  title = dis.readUTF();
  actors = dis.readUTF();
  yearReleased = dis.readLong();
}

In this chapter, we will use a DataOutputStream with a ByteArrayOutputStream underneath to write the object to a record in a record store. In Chapter 7, "Networking," we will use a DataOutputStream associated with an HttpConnection to send the object over an HTTP connection. The nice feature of our simple serialization scheme is that there is no need to change the definition of the Movie class. It is the same, whether it is being used to store movies in a record store or to send movies over an HTTP connection.

Just as we would in J2SE, the way to write objects to a byte stream is to create a DataOutputStream with an underlying ByteArrayOutputStream:

ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);

Once we have created the DataOutputStream, we can pass it to the Movie object and tell it to save itself:

movie.writeObject(dos);  // write the object to the stream

Because the DataOutputStream has an underlying ByteArrayOutputStream, it is easy to obtain the contents of the stream in the form of an array of bytes:

byte[] ba = bos.toByteArray();

To add this byte array to the record store, we use the addRecord method on the RecordStore object:

store.addRecord(ba, 0, ba.length);

To retrieve an object from the record store, we use a DataInputStream with an underlying ByteArrayInputStream. Calling the readObject method on the Movie object reads the byte array from the record and the object is filled with the movie information saved in that record:

ByteArrayInputStream bis = new ByteArrayInputStream(store.getRecord(recordId));
DataInputStream dis = new DataInputStream(bis);
Movie movie = new Movie();
movie.readObject(dis);

To retrieve a collection of records from the record store, RMS provides the RecordEnumeration interface. The RecordEnumeration provides the ability to search forward and backward over the records in the store. To obtain a RecordEnumeration, call the enumerateRecords method on a RecordStore object:

RecordEnumeration re = store.enumerateRecords(this, this, false);

The first parameter provides the ability to apply a filter on the enumeration. The filter is an object that implements the RecordFilter interface, which means it has a matches method. The matches method returns true if the record is to be included in the enumeration. For example, if we wanted all movies with title beginning with "The," we would write a matches method as follows:

public boolean matches(byte[] candidate)
{
  boolean result = true;
  Movie movie = null;
  try {
    ByteArrayInputStream bis = new ByteArrayInputStream(
      candidate);
    DataInputStream dis = new DataInputStream(bis);
    movie = new Movie();
    movie.readObject(dis);
  }
  catch (Exception e) {
    System.out.println(e);
    e.printStackTrace();
  }
  result = movie.title.startsWith("The");
  return result;
}

The matches method is called for each record in the record store, providing a byte array to the method to give it the opportunity to test whether it matches the filter criteria. In the example above, we read the movie object from the DataInputStream (with the underlying ByteArrayInputStream) and test whether its title starts with the characters "The". If so, we return true and the record is included in the enumeration.

If no filter object is supplied, then all records in the record store are included in the enumeration.

The second parameter to the enumerateRecords method provides the ability to specify a sort order in the enumeration. If an object that implements the RecordComparator interface is specified as the second parameter, the interface's compare method will be called for each pair of records in the record store. For example, if we wanted to sort the records by the alphabetic order of the movie titles, we would write a compare method similar to the following:

public int compare(byte[] rec1, byte[] rec2)
{
  Movie movie1 = null;
  Movie movie2 = null;
  try {
    ByteArrayInputStream bis1 =
      new ByteArrayInputStream(rec1);
    DataInputStream dis1 = new DataInputStream(bis1);
    movie1 = new Movie();
    movie1.readObject(dis1);
    ByteArrayInputStream bis2 =
      new ByteArrayInputStream(rec2);
    DataInputStream dis2 = new DataInputStream(bis2);
    movie2 = new Movie();
    movie2.readObject(dis2);
  }
  catch (Exception e) {
    System.out.println(e);
    e.printStackTrace();
  }

  // sort by title
  int result = movie1.title.compareTo(movie2.title);
  if (result < 0) {
    return RecordComparator.PRECEDES;
  }
  else if (result > 0) {
    return RecordComparator.FOLLOWS;
  }
  else {
    return RecordComparator.EQUIVALENT;
  }
}

For each pair of records in the store (after the filter has been applied), the corresponding pair of byte arrays is passed to the compare method. As before, we can convert the byte arrays to Movie objects. If the title of the first movie object lexicographically precedes the second movie object's title, the compare method returns the predefined constant RecordComparator.PRECEDES. If the title of the first movie follows the second movie's title, the compare method returns the predefined constant RecordComparator.FOLLOWS. If they are the same, the method returns RecordComparator.EQUIVALENT.

If no comparator object is supplied to the enumerateRecords method, the records are sorted in an undefined order.