Table of ContentsNetworkingUser Interface (LCDUI)

Persistence (RMS)

When developing a game, you'll want to save (or "persist") data that you can retrieve later, after the gameor even the phoneis shut off. Like most things in the J2ME world, the functionality is there, it's just in the "It's life, Jim, but not as we know it" category.

Persisting data on a MID is done via the RMS (Record Management System), which you'll find in the javax.microedition.rms package (Table 5.7 contains a list of all the classes within this package). The RMS stores data as records, which are then referenced using a unique record key. Groups of records are stored in the rather inventively named record store.

Table 5.7. RMS Package (Excluding Exceptions)

Class

Description

Classes

RecordStore

Allows you access to the record store functionality.

Interfaces

RecordComparator

Provides an interface you can use to implement a comparator between two records (used by enumeration).

RecordEnumeration

Provides an enumerator for a record store; can be used in conjunction with a comparator and a filter.

RecordFilter

Filters record retrieval.

RecordListener

Provides an interface you can use to "listen" to events that occur in the RMS, such as when records are added, changed, or removed.


Record Store

I had real trouble with that heading; so many lame puns, so little time....Anyway, a record store is exactly what the term impliesa storage mechanism for records. You can see the complete API available in Table 5.8.

Table 5.8. javax.microedition.rms.RecordStore

Method

Description

Store Access Methods

static RecordStore openRecordStore (String recordStoreName, boolean createIfNecessary)

Opens a record store or creates one if it doesn't exist.

void closeRecordStore ()

Closes a record store.

static void deleteRecordStore (String recordStoreName)

Deletes a record store.

long getLastModified ()

Gets the last time the store was modified.

String getName ()

Gets the name of the store.

int getNumRecords ()

Returns the number of records currently in the store.

int getSize ()

Returns the total bytes used by the store.

int getSizeAvailable ()

Returns the amount of free space. (Keep in mind that records require more storage for housekeeping overhead.)

int getVersion ()

Retrieves the store's version number. (This number increases by one every time a record is updated.)

static String[] listRecordStores ()

Returns a string array of all the record stores on the MID to which you have access.

Record Access Methods

int addRecord (byte[] data, int offset, int numBytes)

Adds a new record to the store.

byte[] getRecord (int recordId)

Retrieves a record using an ID.

int getRecord (int recordId, byte[] buffer, int offset)

Retrieves a record into a byte buffer. void deleteRecord

(int recordId)

Deletes the record associated with the recordId parameter.

void setRecord (int recordId, byte[] newData, int offset, int numBytes)

Changes the contents of the record associated with recordId using the new byte array.

int getNextRecordID ()

Retrieves the ID of the next record when it is inserted.

int getRecordSize (int recordId)

Returns the current data size of the record store in bytes.

RecordEnumeration enumerate

Records (RecordFilter filter, RecordComparator comparator, boolean keepUpdated) Returns a RecordEnumerator object used to enumerate through a collection of records (order using the comparator parameter).

Event Methods

void addRecordListener (RecordListener listener)

Adds a listener object that will be called when events occur on this record store.

void removeRecordListener (RecordListener listener)

Removes a listener previously added using the addRecordListener method.


As you can see in Figure 5.4, a record store exists in MIDlet suite scope. This means that any MIDlet in the same suite can access that suite's record store. MIDlets from an evil parallel universe (such as another suite) aren't even aware of the existence of your suite's record stores.

Figure 5.4. A MIDlet only has access to any record stores created in the same MIDlet suite.

graphic/05fig04.gif


Record

A record is just an array of bytes in which you write data in any format you like (unlike a database table's predetermined table format). You can use DataInputStream, DataOutputStream, and of course ByteArrayInputStream and ByteArrayOutputStream to write data to a record.

As you can see in Figure 5.5, records are stored in the record store in a table-like format. An integer primary key uniquely identifies a given record and its associated byte array. The RMS assigns record IDs for you; thus, the first record you write will have the ID of 1, and the record IDs will increase by one each time you write another record.

Figure 5.5. A record store contains records, each with a unique integer key associated with a generic array of bytes.

graphic/05fig05.gif


Figure 5.5 also shows some simple uses for a record store. In this example, the player's name (the string "John") is stored in record 1. Record 2 contains the highest score, and record 3 is a cached image you previously downloaded over the network.

NOTE

Note

Practically, you likely would store all the details on a player, such as his name, score, and highest level, as a single record with one record per new player. You would then store all these records in a dedicated Player Data record store.

You might also have noticed that there is no javax.microedition.rms.Record class. That's because the records are just arrays of bytes; all the functionality you need is in the RecordStore class.

Take a look at an example now. In the following code, you'll create a record store, write out some string values, and then read them back again. Doesn't sound too hard, right?

NOTE

Tip

You can see the complete source code for this in the SimpleRMS.java on the CD (Chapter 5 source code).

import java.io.*;
import javax.microedition.midlet.*;
import javax.microedition.rms.*;

/**
 * An example of how to use the MIDP 1.0 Record Management System (RMS).
 * @author Martin J. Wells
 */
public class SimpleRMS extends javax.microedition.midlet.MIDlet
{
   private RecordStore rs;
   private static final String STORE_NAME = "My Record Store";

   /**
    * Constructor for the demonstration MIDlet does all the work for the tests.
    * It firstly opens (or creates if required) a record store and then inserts
    * some records containing data. It then reads those records back and
    * displays the results on the console.
    * @throws Exception
    */
   public SimpleRMS() throws Exception
   {
      // Open (and optionally create a record store for our data
      rs = RecordStore.openRecordStore(STORE_NAME, true);

      // Create some records in the store
      String[] words = {"they", "mostly", "come", "at", "night"};
      for (int i=0; i < words.length; i++)
      {
         // Create a byte stream we can write to
         ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();

         // To make life easier use a DataOutputStream to write the bytes
         // to the byteStream (ie. we get the writeXXX methods)
         DataOutputStream dataOutputStream = new DataOutputStream(byteOutputStream);
         dataOutputStream.writeUTF(words[i]);
         // ... add other dataOutputStream.writeXXX statements if you like 
         dataOutputStream.flush();

         // add the record
         byte[] recordOut = byteOutputStream.toByteArray();
         int newRecordId = rs.addRecord(recordOut, 0, recordOut.length);
         System.out.println("Adding new record: " + newRecordId +
                              " Value: " + recordOut.toString());

         dataOutputStream.close();
         byteOutputStream.close();
      }

      // retrieve the state of the store now that it's been populated
      System.out.println("Record store now has " + rs.getNumRecords() +
                           " record(s) using " + rs.getSize() + " byte(s) " +
                           "[" + rs.getSizeAvailable() + " bytes free]");

      // retrieve the records
      for (int i=1; i <= rs.getNumRecords(); i++)
      {
         int recordSize = rs.getRecordSize(i);
         if (recordSize > 0)
         {
            // construct a byte and wrapping data stream to read back the
            // java types from the binary format
            ByteArrayInputStream byteInputStream = new
ByteArrayInputStream(rs.getRecord(i));
            DataInputStream dataInputStream = new DataInputStream(byteInputStream);

            String value = dataInputStream.readUTF();
            // ... add other dataOutputStream.readXXX statements here matching the
            // order they were written above

            System.out.println("Retrieved record: " + i + " Value: " + value);

            dataInputStream.close();
            byteInputStream.close();
         }
      }


} 

   /**
    * Called by the Application Manager when the MIDlet is starting or resuming
    * after being paused. In this case we just exit as soon as we start.
    * @throws MIDletStateChangeException
    */
   protected void startApp() throws MIDletStateChangeException
   {
      destroyApp(false);
      notifyDestroyed();
   }

   /**
    * Called by the MID's Application Manager to pause the MIDlet. A good
    * example of this is when the user receives an incoming phone call whilst
    * playing your game. When they're done the Application Manager will call
    * startApp to resume. For this example we don't need to do anything.
    */
   protected void pauseApp()
   {
   }

   /**
    * Called by the MID's Application Manager when the MIDlet is about to
    * be destroyed (removed from memory). You should take this as an opportunity
    * to clear up any resources and save the game. For this example we don't
    * need to do anything.
    * @param unconditional if false you have the option of throwing a
    * MIDletStateChangeException to abort the destruction process.
    * @throws MIDletStateChangeException
    */
   protected void destroyApp(boolean unconditional) throws MIDletStateChangeException
   {
   }

}

Seems like a lot of work to write a few strings, doesn't it? Fortunately it's not quite as complicated as it looks. The first thing you did was open the record store using the call rs = RecordStore.openRecordStore(STORE_NAME, true);.

The Boolean argument on the call to openRecordStore indicates that you want to create a new record store if the one you named doesn't already exist.

The next section creates and then writes a series of records to the record store. Because you have to write bytes to the record, I recommend using the combination of a ByteArrayOutputStream and DataOutputStream.

The following code creates our two streamsfirst the ByteArrayOutputStream, and then a DataOutputStream, which has a target of the ByteArrayOutputStream. As you can see in Figure 5.6, this means that any data you write to using the very convenient writeXXX methods of this class will in turn be written in byte array format through the associated ByteArrayOutputStream.

Figure 5.6. DataOutputStreams make byte array formatting easier.

graphic/05fig06.gif


The code to create this "stream train" is

ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
DataOutputStream dataOutputStream = new DataOutputStream(byteOutputStream);

You can then use the DataOutputStream convenience methods to write the data before flushing the stream (thus ensuring that everything is committed to the down streams).

dataOutputStream.writeUTF(words[i]);
dataOutputStream.flush();

Adding the record is simply a matter of grabbing the byte array from the ByteArrayOutputStream and sending it off to the RMS.

byte[] recordOut = byteOutputStream.toByteArray();
int newRecordId = rs.addRecord(recordOut, 0, recordOut.length);

Simple, huh? Here's the output:

Adding new record: 1 Value: [B@ea0ef881
Adding new record: 2 Value: [B@84aee8b
Adding new record: 3 Value: [B@c5c7331
Adding new record: 4 Value: [B@e938beb1
Adding new record: 5 Value: [B@11eaa96
Record store now has 5 record(s) using 208 byte(s) [979722 bytes free]
Retrieved record: 1 Value: they
Retrieved record: 2 Value: mostly
Retrieved record: 3 Value: come
Retrieved record: 4 Value: at
Retrieved record: 5 Value: night

You can see how easily you can write other data using the various write methods in your DataOutputStream. Just be sure you always read things back in the correct order.

Locking

One nice aspect of the RMS implementation is that it takes care of locking for you. The record store implementation guarantees synchronized access, so there is no chance of accidentally accessing storage while another part of your MIDlet, or even another MIDlet in your suite, is hitting it at the same time. Since this type of protection is inherent, you don't need to go to the trouble of coding it yourself.

Enumerating

When you retrieved the records in the previous examples, you used a simple method of reading back the data (an indexing for loop). However, you'll encounter cases in which you want to retrieve only a subset of records, possibly in a particular order.

RMS supports the ordering of records using the javax.microedition.rms.RecordEnumerator class. Table 5.9 lists all the methods in this class.

Table 5.9. javax.microedition.rms.RecordEnumeration

Method

Description

Housekeeping

void destroy ()

Destroys the enumerator.

boolean isKeptUpdated ()

Indicates whether this enumerator will auto-rebuild if the underlying record store is changed.

keepUpdated (boolean keepUpdated)

Changes the keepUpdated state.

void rebuild ()

Causes the enumerator's underlying index to rebuild, which might result in a change to the order of entries.

void reset ()

Resets the enumeration back to the state after it was created.

Accessing

boolean hasNextElement ()

Tests whether there are any more to enumerate in the first-to-last ordered direction.

boolean hasPreviousElement ()

Tests whether there are any more to enumerate in the last-to-first ordered direction.

byte[] nextRecord ()

Retrieves the next record in the store.

byte[] previousRecord ()

Gets the previous record.

int previousRecordId ()

Just gets the ID of the previous record.

int nextRecordId ()

Just gets the ID of the next record.

int numRecords ()

Returns the number of records, which is important when you are using filters.


You can access an enumerator instance using the record store. For example:

RecordEnumeration enum = rs.enumerateRecords(null, null, false);
while (enum.hasNextElement())
{
        byte[] record = enum.nextRecord()  ;
        // do something with the record
            ByteArrayInputStream byteInputStream = new ByteArrayInputStream(record);
            DataInputStream dataInputStream = new DataInputStream(byteInputStream);  
             String value = dataInputStream.readUTF();
             // ... add other dataOutputStream.readXXX statements here matching
             // the order they were written above
        System.out.println(">"+value);
}
enum.destroy();

You can use the enumerator to go both forward and backward through the results. If you want to go backward, just use the previousRecord method.

Comparing

The previous example retrieves records in ID order, but you can change this order using the ...wait for it . . . RecordComparator (the API is shown in Table 5.10).

Table 5.10. javax.microedition.rms.RecordComparator

Method

Description

int compare (byte[] rec1, byte[] rec2)

Returns an integer representing whether rec1 is equivalent to, precedes, or follows rec2.


You can use the javax.microedition.rms.RecordComparator interface as the basis for a class that will bring order to your enumerated chaos. All you need to do is create a class that compares the two records and returns an integer value representing whether one record is equivalent to, precedes, or follows another record.

NOTE

Note

The javax.microedition.rms.RecordComparator interface includes the following convenience definitions for the compare method return values:

RecordComparator.EQUIVALENT=0

The two records are (more or less) the same for the purposes of your comparison.

RecordComparator.FOLLOWS=1

The first record should be after the second.

RecordComparator.PRECEDES=-1

The first record should be before the second.


Here's an example of a record comparator to sort the string values of the previous examples:

class StringComparator implements RecordComparator
{
   public int compare(byte[] bytes, byte[] bytes1)
   {
      String value = getStringValue(bytes);
      String value1 = getStringValue(bytes1);

      if (value.compareTo(value1) < 0) return PRECEDES;
      if (value.compareTo(value1) > 0) return FOLLOWS;
      return EQUIVALENT;
   }

   private String getStringValue(byte[] record)
   {
      try
      {
         ByteArrayInputStream byteInputStream = new ByteArrayInputStream(record);
         DataInputStream dataInputStream = new DataInputStream(byteInputStream);
         return dataInputStream.readUTF(); 
      }
      catch(Exception e)
      {
         System.out.println(e.toString());
         return "";
      }
   }
}

You can then use this comparator in any call to create an enumeration. For example:

RecordEnumeration enum = rs.enumerateRecords(null, new StringComparator(),
false);

If you were to then enumerate through the records from your previous examples, they would be displayed in a sorted order according to the string value stored in each record.

The output from running this against your previous record store's data follows. (Sounds uncannily like Yoda, doesn't it?)

Retrieved record: 4 Value: at
Retrieved record: 3 Value: come
Retrieved record: 2 Value: mostly
Retrieved record: 5 Value: night
Retrieved record: 1 Value: they

If you have records containing more complicated data, you need to have your comparator make a logical appraisal of the contents of each record, and then return an appropriate integer to represent the desired order.

You can see a complete working example of this in the SortedRMS.java file on the CD (in the Chapter 5 source code).

Filtering

Sorting enumerations is great! I can't think of anything else I'd like to be doing on a Saturday than sorting enumerators, but sometimes you'll want to limit or filter the records you get back from an enumerator. Enter the javax.microedition.rms.RecordFilter class.....You can see the one and only method in this class defined in Table 5.11.

Table 5.11. javax.microedition.rms.RecordFilter

Method

Description

boolean matches (byte[] candidate)

Returns true if the candidate record validly passes through the filtering rules.


Filtering is just as easy as comparing. (I'm assuming you found that easy.) You just create a class that implements the javax.microedition.rms.RecordFilter and then implement the required methods. Here's an example:

class StringFilter implements RecordFilter
{
   private String mustContainString;

   public StringFilter(String mustContain)
   {
      // save the match string
      mustContainString = mustContain;
   }

   public boolean matches(byte[] bytes)
   {
      // check if our string is in the record
      if (getStringValue(bytes).indexOf(mustContainString) == -1)
         return false;
      return true;
   }

   private String getStringValue(byte[] record)
   {
      try
      {
         ByteArrayInputStream byteInputStream = new ByteArrayInputStream(record);
         DataInputStream dataInputStream = new DataInputStream(byteInputStream);
         return dataInputStream.readUTF();
      }
      catch (Exception e)
      {
         System.out.println(e.toString());
         return "";
      }
   }
}

To use your new filter, just instantiate it in the call to construct the enumerator, like you did with the comparator.

RecordEnumeration enum = rs.enumerateRecords(new StringFilter("o"),
new StringComparator(), false);

Note that I'm using the comparator and the filter together. It's all happening now!

The output from this is now limited to records with a string value containing an "o" (the result of the indexOf("o") call returning something other than 1). Thus you would only see

Retrieved record: 3 Value: come
Retrieved record: 2 Value: mostly

Listening In

The RMS also has a convenient interface you can use to create a listener for any RMS events that occur. Using this, you can make your game react automatically to changes to a record store. This is especially important if such a change has come from another MIDlet in your suite because you won't be aware of the change.

To create a listener, you need to make a class that implements the javax.microedition. rms.RecordListener interface (Table 5.12 lists the methods).

Table 5.12. javax.microedition.rms.RecordListener

Method

Description

void recordAdded (RecordStore recordStore, int recordId)

Called when a record is added.

void recordChanged (RecordStore recordStore, int recordId)

Called when a record is changed.

void recordDeleted (RecordStore recordStore, int recordId)

Called when a record is deleted.


For example, the following is an inner class that simply outputs a message whenever an event occurs:

class Listener implements javax.microedition.rms.RecordListener
{

   public void recordAdded(RecordStore recordStore, int i)
   {
      try
      {
         System.out.println("Record " + i + " added to " + recordStore.getName());
      }
      catch(Exception e)
      {
         System.out.println(e);
      }
   } 

   public void recordChanged(RecordStore recordStore, int i)
   {
      try
      {
         System.out.println("Record " + i + " changed in " + recordStore.getName());
      }
      catch (Exception e)
      {
         System.out.println(e);
      }
   }

   public void recordDeleted(RecordStore recordStore, int i)
   {
      try
      {
         System.out.println("Record " + i + " deleted from " + recordStore.getName());
      }
      catch (Exception e)
      {
         System.out.println(e);
      }
   }
}

NOTE

Note

The source file ListenerRMS.java on the CD has a complete example of a working RMS Listener.

To activate this listener, use the RecordStore.addListener method on a record store you have previously created. This can be used anywhere where you have a record store and want to be notified when it is accessed, for example:

RecordStore rs = RecordStore.openRecordStore("Example", true);
rs.addRecordListener(new Listener());

Exceptions

I want to make a quick note about exceptions before you leave the world of RMS. In the preceding examples, I've ignored or hacked up pathetic handlers for run-time exceptions that might be thrown. Table 5.13 lists all of these exceptions.

Table 5.13. RMS Exceptions

Exception

Description

InvalidRecordIDException

Indicates that an operation could not be completed because the record ID was invalid.

RecordStoreException

Indicates that a general exception occurred in a record store operation.

RecordStoreFullException

Indicates that an operation could not be completed because the record store system storage was full.

RecordStoreNotFoundException

Indicates that an operation could not be completed because the record store could not be found.

RecordStoreNotOpenException

Indicates that an operation was attempted on a closed record store.


In most cases RMS exceptions occur due to abnormal conditions in which you need to either code around the problem (in the case of the RecordStoreNotFoundException, RecordStoreNotOpenException, and InvalidRecordIDException) or just live with it (in the case of RecordStoreException). The one possible exception to this is RecordStoreFullException, which you might resolve by having your MIDlet clear some space and try again.

Either way, when developing your game, consider the implications of these exceptions and do your best to handle them gracefully, even if all you can do is inform the player that his game save failed.

    Table of ContentsNetworkingUser Interface (LCDUI)