The J2ME Record Store
Peter Komisar           ©           Conestoga College     v.1.1 

references: 'Databases and MIDP, Part 1: Understanding the
Record Management System',
Eric Giguere, Sun Microsystems,
J2ME record management store,  Soma Ghosh, IBM, 'Learning
Java Wireless', Qusay Mahmoud, O'Reilly, MIDP Database
Programming Using RMS: a Persistent Storage for MIDlets,
Qusay Mahmoud, Sun MicroSystems


The Record Management System

The MIDP supplies a record-oriented data storage
model called the 'Record Management System' for
storing data persistently across sessions using the
mobile device.

The RMS is an API that provides local, on-device
persistent storage for data. This is the only storage
area defined in J2ME.


The Record Store

In the model records are stored in a record store.

Each 'record store' may be thought of as a collection
of records. These records persist across multiple
invocations of a MIDlet. The device platform has
the responsibity to maintain the data records across
normal use of the device including reboots and
battery changes. 

Records

A record is an individual data item, which may
contain any Java type, including numbers, strings,
arrays, or object types. This is so because all the
types are represented as a sequence of bytes.

RMS Records Have No Fields

RMS records have no fields. Instead it is a single
binary field of variable size. How the field is interpreted
depends on the application using it. The RMS provides
the storage area and a unique identifier and that is it.

At the API level, records are simply byte arrays.

More on the Record Store

Formally speaking the record store is an ordered
collection of records.

Records are not independent of their container.
Each record belongs to the record store, similar
to how a class member belongs to a class.

The record store is also responsible for maintaining
'atomicity' for the records, ensuring there is no danger
of data corruption.

// atomicity means the data object will be read in full during an
// operation. An int assignment is atomic
in that the assignment
// once started will always complete
in every circumstance


Record ID

On creation, a unique identifier, called the record ID is
assigned to the record by the record store. The first
record added is given the value, 1, the second, 2, and
so on.

A record ID should not be thought of as an index as
record deletions do not renumber existing records
or change the value of the next record ID. Record
identifiers are like T. Codd's database rules where
keys are not reused.

Record Store Names Within the MIDlet Suite

Within a MIDlet suite, names are used to identify record
stores. A record store name may have 1 to 32 Unicode
characters, and must be unique within the MIDlet suite in
which the record was created.

While in the earlier version of MIDP, version 1.0 record
stores could not be shared between MIDlets, MIDP 2.0
provides the option for sharing a record store between
MIDlet suites.

In this case the record store is identified by the names
of the its vendor, and the record store name itself.

// a more qualified name

Details

"Record stores also maintain time-stamp and version
information so applications can discover when a record
store was last modified. For close tracking, applications
can register a listener to be notified whenever a record
store is modified."    
 
 - 'Understanding the Data Management System', Eric Giguere,
   Sun Microsystems

          

The RecordStore Class

At the programming level, the record store is represented
by the RecordStore class found in the javax.microedition.rms
package.

The RecordStore's Package

import javax.microedition.rms;

RMS Characteristics

Storage Limits

Available memory available will vary from device to
device. The MIDP specification requires a reserve of
at least 8K of non-volatile memory for persistent data
storage.

// at least 8K

No limitations are made by the specification on the size
of individual records, but constraints may vary between
devices.

Methods for determining an individual record's size,
the total size of a record store, and how much memory
remains are supplied.

Programming Practice

A MIDlet using RMS should specify the minimum number
of bytes of storage it requires. This is done by setting the
MIDlet-Data-Size attribute in the JAR manifiest and the
application deployment descriptor.

By keeping the request minimal, a device is less likely
to refuse to install the MIDlet. If the attribute is missing
the assumption is that no space is required.

In practice, devices may allow applications to exceed
their stated requirements, but this behavior cannot be
relied upon.

// some implementations require defining additional attributes
// check the device documentation

Speed

Operations using persistent memory will normally be slower
than an equivalent operation that uses non-persistent memory.

In the above referenced Sun article,  Eric Giguere, states
that writing data, in particular, can take a long time on some
platforms. So, for better performance, caching frequently
accessed data in volatile memory can help keep the user
interface responsive. It is also recommended that RMS
operations not be executed from the MIDlet's event thread.

The javax.microedition.rms Package

Once again an approach similar to the javax.microedition.io
package is observable where there a more interfaces than
classes. Also, a single controller class is used to control
all aspects of data record management.

Each of the interfaces supplies a different function
for the package.


RMS Package Interfaces


The RMS Package Class

The Exception Hierarchy is shown below.


Exceptions

In addition to the java.lang.IllegalArgumentException, the
following exceptions may be thrown when using the RMS.

RecordStoreException Hierarchy

RecordStoreException
     |___InvalidRecordIDException
     |___RecordStoreFullException
     |___RecordStoreNotFoundException
     |___RecordStoreNotOpenException


RMS Exceptions

The RecordStore Methods

The RecordStore class has a large number of
methods for manipulating the data in a record
store as well as interrogating the device's physical
environment.

Following are the methods with modifiers and
parameters removed.

RecordStore Methods
// without modifiers, return types and parameter types

The Record Store In Use

Opening a Store

A static call on the openRecordStore( ) method opens
and possibly creates a record store. The first parameter
names the store while the boolean second parameter,
indicates to create if neccessary.

There are three openRecordStore( ) methods shown
below. They allow the store to be associated in different
ways, with this MIDlet, a given MIDlet and to share with
other MIDlets.


The openRecordStore( ) Methods
// from the J2ME 2.5.2 documentation

  static RecordStore  openRecordStore
            (String recordStoreName, boolean createIfNecessary)    
           // open (and possibly create) a record store
            // associated with the given MIDlet suite.

 static RecordStore  openRecordStore
           (String recordStoreName, boolean createIfNecessary,
                                                     int authmode, boolean writable)
        
// open (and possibly create) a record store
         // that can be shared with other MIDlet suites.
 static RecordStore  openRecordStore
(String recordStoreName, String vendorName, String suiteName)
          // open a record store associated with the named MIDlet suite.


Constants Used in openRecordStore( )

In the second of the above methods an authmode variable
controls whether or not the record store may be shared.

RecordStore Constants 
// static ints


Example

RecordStore.openRecordStore( "Messages", true, 
RecordStore.AUTHMODE_ANY, true );

// where true signifies it is writable

The setMode( ) Method

The setMode( ) method can change the writability
and access of a record store at any time.


Example

setMode( RecordStore.AUTHMODE_ANY, false );

The openRecordStore( ) is declared as throwing both 
the  child exception RecordStoreNotOpenException
and it's parent, the  RecordStoreException so opening
record stores will usually be done in the context of try
catch blocks.

Following three records stores are opened.


Example
 

 try{   
      RecordStore rs1 =
      RecordStore.openRecordStore("Records",true);
      RecordStore rs2 =
      RecordStore.openRecordStore("Passwords",true);
      RecordStore rs3 =
      RecordStore.openRecordStore("Ph_Addressess",true);
      throw new RecordStoreException( );
      }
  catch(RecordStoreException rse)
      {form.append(rse.toString( ) );
      }



Discovering Stores


The listRecordStores( ) method returns the names
of the existing record stores on the device.


Example

String[] names = RecordStore.listRecordStores();
    
    for( int i = 0; names != null &&
               i < names.length; ++i )
           {
           form.append( "Store " + (i+1) + ": " + names[i] );
           }


// the example assumes you have a handle to a Form object


Closing a Record Store


The closeRecordStore( ) method is called on the
reference for the record store that is being closed.

Caution! This method is a little 'wiley', as is indicated
by the following quote from the methods documentation.

"Note that the record store will not actually be closed
until closeRecordStore() is called as many times as
openRecordStore() was called. In other words, the
MIDlet needs to make a balanced number of close
calls as open calls before the record store is closed."

The closeRecordStore( ) method also throws two
exceptions, RecordStoreNotOpenException and
the parent,  RecordStoreException, in the event
that it attempts to close a record store that is not
open, among other things.

The Sun author put the call to close a record inside
a finally block which independently handles this
possibility.

Example

finally {
        try {
             rs3.closeRecordStore();
             } catch( RecordStoreException e ){
            // if you have a form in your MIDlet
              form.append(e.toString());
            }
       }


A more elaborate form of this code is shown
in later code samples.


The following code can be used to experiment
with adding and removing record stores.


Sample for Experimenting With Adding and
Removing Record Stores

import javax.microedition.lcdui.*;
import javax.microedition.midlet.MIDlet;
import javax.microedition.rms.*;

public class Records extends MIDlet{

public void startApp() {
   Display display = Display.getDisplay(this);
  
   RecordStore rs3=null;
   Form form = new Form("Record Store");
   form.append("Record Store Demo");
   form.append("\n=====================\n");
 
  try{ // opening record stores
      
      RecordStore rs1 =
      RecordStore.openRecordStore("Records",true);
      RecordStore rs2 =
      RecordStore.openRecordStore("Passwords",true);
      rs3 =
      RecordStore.openRecordStore("Ph_Addressess",false);
   
   
  // set to false so an attempt to open a store
     // is made but not to create it
         
     // use to test the Exception message append
     //   throw new RecordStoreException( );
      }
  catch(RecordStoreException rse)
      {form.append(rse.toString());
      }


   // 1. Discovery

  String[] names = RecordStore.listRecordStores( );
    
    for( int i = 0; names != null &&
               i < names.length; ++i )
           {
           form.append( "\nStore : " + names[i] );
           }

   form.append("\n=====================\n");
   display.setCurrent(form);
}
public void destroyApp(boolean unconditional) {}
public void pauseApp(){}
}


Adding Records with the addRecord( ) Method

Records are added with the addRecord( ) method.
This is described as a blocking atomic operation,
which means it cannot be interrupted until it completes.

You can see from the signature that the developer
has the task of converting the data to be stored to
a byte array. One thing that makes life easier is that
the byte array has a length variable to supply the
length of the array.

This method also throws a set of exceptions. The
addRecord( ) method has the following form.


The addRecord( ) Method Signature

public int addRecord(byte[] data, int offset,  int numBytes)
              throws RecordStoreNotOpenException,
                     RecordStoreException,
                     RecordStoreFullException



If the record has zero-length as in no data, the byte
array may be shown as null,. Similarly the numbBytes
parameter may be zero.


Details Regarding Going In and Out of the byte Array


The following code enters six records. The sort
of record we think about in relational databases
are compound records. The equivalent in this
model is records 1 to 3 and 4 to 6. We extract
the name and phone numbers by manipulating
the index system supplied by the record store.

We need to brush up on some of the conversion
methods.  For instance for a String there is a
getBytes( ) method that will return a byte array
for that String. The length variable of the byte
array gives it's length. This ideas are shown
in the short regular Java code example

Example

class Solar{
  public static void main(String[] args){
    String ray = "Sun";
    byte[] sunBytes=ray.getBytes();
     for (int i=0;i<sunBytes.length;i++){
       System.out.println(sunBytes[i] +"~~~");
       }
  }
}

OUTPUT
 
> java Solar                                     
 ~~~ S ~~~  ~~~ u ~~~  ~~~ n ~~~

In reverse we can create a String object using
the String constructor that takes a byte[] array.

Example

class Word{
  public static void main(String[] args){
   byte[] word = { 67, 65, 84, 83 };
   String string=new String(word);
  System.out.println(string);
  }
}

OUTPUT

> java Word        
CATS                  


This is enough conversion power to allow us
to enter and retrieve String objects from the
record store.


Adding and Retrieving Records Sample

import javax.microedition.lcdui.*;
import javax.microedition.midlet.MIDlet;
import javax.microedition.rms.*;

public class Contacts extends MIDlet
{
  Display display;
  RecordStore contacts;
  Form form;

public void startApp(){
   display = Display.getDisplay(this);
   form = new Form("Contacts");
   try
     { 

     form.append("Name and Phone Number");
     form.append("~~~~~~~~~~~~~~~~~~~~~~");     
     contacts =
     RecordStore.openRecordStore("Contacts",true);   
    
     // enter some base entries,
     // can be a simple array of one element
    
   
     byte[] name = ("Walter Cronkite").getBytes( );
     byte[] address= ("44 TV Blvd.,CA, US").getBytes( );
     byte[] phoneNo=("409-223-2356").getBytes( );
  
     contacts.addRecord(name,0,name.length);
     contacts.addRecord(address,0,address.length);
     contacts.addRecord(phoneNo,0,phoneNo.length);

 
     byte[] name2 = ("Wilma Crocket").getBytes( );
     byte[] address2=("11 Vue St.,Phelps,QE,CA").getBytes( );
     byte[] phoneNo2=("613-773-8899").getBytes( );  

    
     contacts.addRecord(name2,0,name2.length);
     contacts.addRecord(address2,0,address2.length);
     contacts.addRecord(phoneNo2,0,phoneNo2.length);
     int num= contacts.getNumRecords();
     num=num+1;
    
  
     byte[] name_=null;
     byte[] phone_=null;
    
     // tricking the loop to emulate a
     // a record like in a database

     for (int i=1;i< num ; i=i+3)
       {
         name_  = contacts.getRecord(i);
         phone_ = contacts.getRecord(i+2);
         String str_name=new String(name_);
         String str_phone=new String(phone_);

        form.append("Name: Ph #: " + str_name +
                     "---"         + str_phone  );   
        }


     }
     catch(RecordStoreException rse)
       {
       System.out.println("Is this an error?");
       }      
 
     display.setCurrent(form);
   }

   public void destroyApp(boolean unconditional) {}
   public void pauseApp(){}
 }



Using Stream Classes to Read and Write Methods

Stream classes can be used in this process as shown
in the Qusay Mahmoud article reference above. It is
really just an augmentation on the same process we
described earlier.


Writing Example
// from MIDP Database Programming Using RMS: a Persistent
// Storage for MIDlets, Qusay Mahmoud, Sun MicroSystems


ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
dos.writeUTF(record);
byte[] b = baos.toByteArray();
db.addRecord(b, 0, b.length);


Reading Example

ByteArrayInputStream bais = new
ByteArrayInputStream(record1);
DataInputStream dis = new
DataInputStream(bais);
String in = dis.readUTF();

The merits of this do not show that well in a simple
example. They would be useful though if you were
storing and reading Java primitives with the Data
Input and Output Streams. Here is an example for
the record.


Caution!

Running the code a great number of times seemed to
have unintended effects on the record store. The finally
clause can be commented back into the code to start
with a  clean record store.

// may no longer apply to WTK3.0

Recall that the RecordStoreNotFoundException is
thrown when the number of times the record is open
is equal to the number of times it has been closed.

Example


/*

finally{
       boolean flag=false;
        try{
           while(flag == false)
              {
              members.closeRecordStore();
              }
           }
        catch(RecordStoreException rse)
          {
          // also catches RecordStoreNotFoundException
          form.append(rse.toString());
          flag=true;
          }
       } // end of finally
*/

The following example is long winded but it does
demonstrate the additional use of streams in
creating records.

Example Writing to records using Streams

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

public class Members extends MIDlet
{
  Display display;
  Form form;
   RecordStore members;

public void startApp(){
   display = Display.getDisplay(this);
   form = new Form("Contacts");
   form.append("Member___Since\n");
   form.append("~~~~~~~~~~~~~~~~~~~~~~\n");  
   try
     {      
     members=RecordStore.openRecordStore
              ("Members",true);      
  
    String member1 = "Montgomery,1988";
    String member2 = "Alicia,1951";
    String member3 = "Raj,2001";

    // streams are closed and nulled to make
    // unused objects free for garbage collection

    ByteArrayOutputStream baos1 = new ByteArrayOutputStream();
    DataOutputStream dos1 = new DataOutputStream(baos1);
    dos1.writeUTF(member1);
    byte[] b1 = baos1.toByteArray();
    members.addRecord(b1, 0, b1.length);
    dos1.close();
    baos1.close();

    ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
    DataOutputStream dos2 = new DataOutputStream(baos2);
    dos2.writeUTF(member2);
    byte[] b2 = baos2.toByteArray();
    members.addRecord(b2, 0, b2.length);
    dos2.close();
    baos2.close();

    ByteArrayOutputStream baos3 = new ByteArrayOutputStream();
    DataOutputStream dos3 = new DataOutputStream(baos3);
    dos3.writeUTF(member3);
    byte[] b3 = baos3.toByteArray();
    members.addRecord(b3, 0, b3.length);
    dos3.close();
    baos3.close();

    // Reading back
  
   String result1;  
   ByteArrayInputStream bais1 = new
   ByteArrayInputStream(members.getRecord(1));
   DataInputStream dis1 = new DataInputStream(bais1);
   result1 = dis1.readUTF();
   form.append(result1 + "\n");
 
   dis1.close();
   bais1.close();
   bais1=null;    
   dis1=null;
   result1=null;
    
   String result2; 
   ByteArrayInputStream bais2 = new
   ByteArrayInputStream(members.getRecord(2));
   DataInputStream dis2 = new DataInputStream(bais2);
   result2 = dis2.readUTF();
   form.append(result2 + "\n");
 
   bais2.close();
   dis2.close();
   bais2=null;    
   dis2=null;
   result2=null;

   String result3;
   ByteArrayInputStream bais3 = new
   ByteArrayInputStream(members.getRecord(3));  
   DataInputStream dis3 = new DataInputStream(bais3);
   result3 = dis3.readUTF();
   form.append(result3 + "\n");

   bais3.close();
   dis3.close();
   bais3=null;    
   dis3=null;
   result3=null;   
    
     }
     catch(RecordStoreException rse)
       {
       System.out.println(rse.toString());
       }  
     catch(IOException io)
       {
       System.out.println(io.toString());
       }  

 // comment finally in to get a fresh start with the record store

/*
finally{
       boolean flag=false;
        try{
           while(flag == false)
              {
              members.closeRecordStore();
              }
           }
        catch(RecordStoreException rse)
          {
          // also catches RecordStoreNotFoundException
          form.append(rse.toString());
          flag=true;
          }
       }
*/
     display.setCurrent(form);
   }
   public void destroyApp(boolean unconditional) {}
   public void pauseApp(){}
 }


Updating Records

The setRecord( ) method may be used to update
a record.  It may set a record as having the most
exceptions thrown of any method in the Java APIs!

You can catch for all of them by trapping the parent,
RecordStoreException. Printing the associated
reference toString( ) gives a description of what
exception was actually thrown.

It's first parameter is the record ID for which the
update is being executed.


The setRecord( ) Method Signature

public void setRecord(int recordId, byte[] newData,
                      int offset,  int numBytes)
               throws RecordStoreNotOpenException,
                           InvalidRecordIDException,
                           RecordStoreException,
                          RecordStoreFullException


Following is code snippet showing how the method
is used.


Example
// a store with reference variable, birds


 
String balt = "Baltimore Oriole";
    byte[] bytes = balt.getBytes();
    birds.setRecord(5, bytes, 0, bytes.length);


Deleting Records

The deleteRecord( ) method is used to delete( ) a
record.

The deleteRecord( ) signature is as follows.


The deleteRecord( ) Method Signature

public void deleteRecord(int recordId)
                  throws RecordStoreNotOpenException,
                         InvalidRecordIDException,
                         RecordStoreException


Example

storeRef.deleteRecord(1);


Comparisons   // the RecordComparator Interface

What if you don't know the record number. Perhaps
you only know the value of the record. Comparisons
of records are based on the compare( ) method.


The compare( ) Method Signature

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

// the int returned is equal to one of the following constants
 

It's use is based on the following three constants.

RecordComparator Interface Constants

Enumerations // the RecordEnumeration Interface

Enumerations of a record store data are obtained
by using the enumerateRecords( ) method found in
the RecordStore class. It has the following signature.


The enumerateRecords( ) Method Signature

public RecordEnumeration enumerateRecords
      (RecordFilter filter,  RecordComparator comparator,
                 boolean keepUpdated)  throws
                           RecordStoreNotOpenException


It can use a filter, represented by the RecordFilter class
that allows pattern matching with the match( ) method.
In the following example, there is no filter or comparator
passed into the method.


Example

RecordEnumeration re = rs.enumerateRecords(null, null, false);
If (re.hasNextElement( ))
byte nextRecord[ ] = re.nextRecord( );


We will build a final example to show the modify and
delete actions. Note the delete invalidates that key. One
author maintained a Vector of keys and used it to control
the presentation of records.


Modify and Delete Example

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

public class Birds extends MIDlet
{
  Display display;
  Form form;
  RecordStore birds;
  byte[] birdRay;
  String[] birdStrings;

public void startApp(){
   display = Display.getDisplay(this);
   form = new Form("Birds");
   form.append("Birds");
   form.append("\n~~~~~~~~~~~~~~~~~~~~~~\n");  
   try
     {      
     birds=RecordStore.openRecordStore("Birds",true);
     birds.setMode(RecordStore.AUTHMODE_ANY,true);  
       
     birdStrings=new String[]
{"Robin","Canary","Sparrow","Hawk","Oriole","Owl","Pelican","Crow"};
    

    for(int i=0;i<birdStrings.length;i++){
       byte[] birdRay = birdStrings[i].getBytes();   
       birds.addRecord(birdRay,0,birdRay.length);
       int j=i+1;
      form.append(new String(birds.getRecord(j))+"\n");
       }  

     form.append("\n==========Update==========\n");
  
      // the update    

     String balt = "Baltimore Oriole";
      byte[] bytes = balt.getBytes();
     birds.setRecord(5, bytes, 0, bytes.length);
     form.append(new String(birds.getRecord(4))+"\n");
     form.append(new String(birds.getRecord(5))+"\n");
     form.append(new String(birds.getRecord(6))+"\n");
 
      // the delete

     form.append("\n==========Delete==========\n");
     birds.deleteRecord(1); // the Robin

    
     // but this throws exception as 1 is gone
     form.append(new String(birds.getRecord(1))+"\n");
        

     }
     catch(RecordStoreException rse)
       {
       form.append(rse.toString());
       } 

// might be needed to close record store
/*
       finally{
       boolean flag=false;
        try{
           while(flag == false)
              {
              birds.closeRecordStore();
              }
           }
        catch(RecordStoreException rse)
          {
          // also catches RecordStoreNotFoundException
          form.append(rse.toString());
          flag=true;
          }
       } // end of finally  
*/
        display.setCurrent(form);
   }

   public void destroyApp(boolean unconditional) {}
   public void pauseApp(){}
 }



Assignment


1) Create a MIDlet that creates a record store called
products and store in it five entries. Read the entries
from the record store and display them to screen.

Screenshot the code output.

2) Using the same code modify two records and delete
one of the others.  Submit the new screenshot showing
the effects of the revised code.

// You can do it all in one view as in the above example
// if you wish