IBM Skip to main content
Search for:   within 
      Search help  
     IBM home  |  Products & services  |  Support & downloads   |  My account

developerWorks > Java technology
developerWorks
J2ME 101, Part 3: Inside the Record Management System
203 KBe-mail it!
Contents:
Record management in MIDP
Don't miss the rest of this series!
Creating a record store
The RecordStore API
The ReadWrite MIDlet
Read and write Java primitives
The RecordEnumeration API
The RecordComparator API
Sorting multiple data types
The RecordFilter API
The StringSearch MIDlet
The RecordListener API
Conclusion
Resources
About the author
Rate this article
Related content:
WebSphere Micro Environment
MIDlet development with the WTK
The record management system
The IBM Developer Kits for the Java platform (downloads)
Subscriptions:
dW newsletters
dW Subscription
(CDs and downloads)
Persistent storage in J2ME and MIDP

Level: Introductory

John Muchow (mailto:John@CoreJ2ME.com?cc=&subject=Inside the Record Management System)
Developer and Author
12 December 2003

MIDP does not use a filesystem to save application data. Rather, MIDP stores all information in non-volatile memory, using a storage system called the Record Management System (RMS). In In this article, the first in a two-part companion series to the J2ME 101 tutorial series, author and developer John Muchow introduces the basics of the RMS application interface, then walks you through several development examples that illustrate its functionality.

Data storage and retrieval is an essential aspect of application development. The data that is stored will depend on the type and complexity of the application. In some cases the only persistent data to be stored is the application user preferences. In other cases you may need to store and manage a repository of contact information. On the high end, you could be designing a data storage solution for a full range of supply-chain data. No matter what the case, there is just one all-purpose data storage and management solution for MIDP-enabled devices, and that is the Record Management System (RMS).

In this article, the first of a companion series to the two-part J2ME 101 tutorial series, we'll explore the inner workings of MIDP's persistent storage system. We'll start with a quick overview of RMS, but most of the article (like the tutorial series) will be hands-on. Together, we'll build MIDlets that will help you understand how data records are written to and read from the RMS, as well as the variety of sorting, searching, and retrieval options that are available within this remarkably well-rounded and compact data management system.

Please note that this article assumes that you are familiar with MIDlet development in the J2ME environment. You will need to have a J2ME development environment installed on your system in order to compile the code examples. See the Resources section for links to installation instructions for the J2ME Wireless Toolkit (WTK).

Record management in MIDP
Put simply, the MIDP Record Management System (RMS) provides a means to store application data that persists across invocations of a MIDlet. You can visualize the RMS record store as a very simple database, where each row consists of two columns: one containing a unique row identifier, the other a series of bytes representing the data in the record. Table 1 illustrates a simple record store database.

Table 1. A record store database
Record ID Data
1 Array of bytes
2 Array of bytes
3 Array of bytes
... ...

Don't miss the rest of this series!
Part 1: Introduction to MIDP's high-level interface (November 2003) provides a step-by-step introduction to the components that facilitate the main interaction between the user and the device display.

Part 2: Introduction to MIDP's low-level interface (December 2003) walks through the basics of creating and working with the Canvas and Graphics classes, and provides a brief overview of the Game API, introduced with MIDP 2.0.

Part 4: The Generic Connection Framework (January 2004) examines the Generic Connection Framework.

The unique row identifer is an integer value. The first entry will have the ID of 1, the next of 2, and so on. Row identifiers are not re-used if a row is deleted. That is, if we have three rows in a table with the IDs 1, 2, and 3, deleting ID 2 will remove this identifier permanently from the record store. If we were to add another row to this table, it would have an identifier of 4.

Record stores are identified by name. A name may consist of up to 32 characters, and all characters are case sensitive. No two record stores within a MIDlet suite (that is, a collection of one or more MIDlets packaged together) may contain the same name.

Each record store has a version number as well as a date/time stamp. Both values are updated whenever a record is added, replaced, or deleted.

Creating a record store
No constructor exists for creating a record store. Instead, we use a set of three dual-purpose methods to create and/or open record stores. Each method is shown in Listing 1.

Listing 1. Creating and opening record stores

RecordStore openRecordStore(String recordStoreName,
                               boolean createIfNecessary)
RecordStore openRecordStore(String recordStoreName,
                               boolean createIfNecessary,
                               int authmode,
                               boolean writable)
RecordStore openRecordStore(String recordStoreName,
                               String vendorName,
                               String suiteName)

Deleting a record store
When a MIDlet suite is deleted from a device, any record stores created by the suite will also be removed.

The first method opens the named record store if it exists. If the named record store does not exist, and if the parameter createIfNecessary is set to true, the method can be used to create a new (named) record store. The second method operates in the same fashion but employs two additional parameters to specify the record store's access restrictions. The first parameter specifies whether or not only those MIDlets in the same suite can access the record store. The second one specifies whether MIDlets that have access to the record store, can create new records. The last method provdes a means for a MIDlet to open a record store in another MIDlet suite.

The RecordStore API
A handful of methods are available for working with a record store. These range from adding, deleting, and replacing record contents to enumerating through a record store. Each available method is shown in Listing 2.

Listing 2. Record store methods

void closeRecordStore()
void deleteRecordStore(String recordStoreName)
String[] listRecordStores()
int addRecord(byte[] data, int offset, int numBytes)
void setRecord(int recordId, byte[] newData, int offset, int numBytes)
void deleteRecord (int recordId)
byte[] getRecord (int recordId)
int getRecord (int recordId, byte[] buffer, int offset)
int getRecordSize (int recordId)
int getNextRecordID()
int getNumRecords()
long getLastModified()
int getVersion()
String getName()
int getSize()
int getSizeAvailable()
RecordEnumeration enumerateRecords(RecordFilter filter,
                                      RecordComparator comparator,
                                      boolean keepUpdated)
void addRecordListener (RecordListener listener)
void removeRecordListener (RecordListener listener)
void setMode(int authmode, boolean writable)

Locking a record
Unlike a traditional database, RMS offers no methods to lock a record store. It is up to the device implementation to ensure that all operations are synchronous. Should you use separate threads to access a record store, it is up to you to ensure that threads accessing resources do not interfere with each other.

You'll learn more about the RecordStore API and many of its methods as we work through the examples in the sections that follow.

The ReadWrite MIDlet
Our first illustrative example is the ReadWrite MIDlet. This MIDlet has the ability to create a record store, write several records into persistent storage, read back those same records, and delete the record store upon exiting. As you study the code below, note that the MIDlet contains several "convenience" methods, which are methods used time and again when working with the RMS. Convenience methods are the methods used to open, close, and delete record stores.

Take a look at the complete code for the ReadWrite MIDlet and then we'll discuss it in more detail.

Notes about the code
A couple of points are worth mentioning before we move on. First, note that in this example we created a new record store by passing true for the createIfNecessary parameter when calling RecordStore.openRecordStore(REC_STORE, true), as previously explained.

Second, when we wrote to the record store with writeRecord(String str), we first converted the Java string parameter into a byte array. This byte array was then passed to addRecord(rec, 0, rec.length) to insert a record into the record store.

Finally, in the readRecords() method we allocated a byte array to store any record data we read from the RMS. This allows us to make a specific check on each read to ensure that the array is large enough to hold the data. Once the record has been retrieved, we can output the contents to the console.

Figure 1 shows the output of the ReadWrite MIDlet when it is run from within the J2ME WTK.

Figure 1. Record output from the ReadWrite MIDlet
Figure 1. Record output from the ReadWrite MIDlet

Read and write Java primitives
The ReadWrite MIDlet only writes text strings to the record store. For the next example, we'll add the ability to store and manipulate arrays of integer, boolean, and string values. We'll also add support for reading and writing using Java streams. As with the previous example, we'll write several records and then read them back from the store.

As with any MIDlet that needs to access the record store, we begin by using the openRecordStore() method to allocate and open (or create) a record store, as shown in Listing 3.

Listing 3. Create a record store

private RecordStore rs = null;    // Record store
...

public void openRecStore()
{
     ...
     // Create record store if it does not exist
     rs = RecordStore.openRecordStore(REC_STORE, true);
     ...
}

Listing 4 shows the primitive data types we'll write to the record store.

Listing 4. Java primitive data types

public void writeTestData()
{
     boolean[] booleans = {true,false};
     int[] integers = {17 , 4};
     String[] strings = {"Golf", "Tennis"};

     writeStream(booleans, integers, strings);
}

If you're familiar with using streams in the Java language, you'll find little difference between working with a MIDlet and with a more traditional Java application. The steps are as follows:

  • Allocate streams
  • Write the data
  • Flush the stream
  • Transfer the stream data into an array
  • Write the array to the record store
  • Close the streams

Listing 5 shows how to write to RMS using a stream:

Listing 5. Writing a stream to the record store

public void writeStream(boolean[] bData, int[] iData, String[] sData)
{
     try
     {
        // Write data into an internal byte array
       ByteArrayOutputStream strmBytes = new ByteArrayOutputStream();

       // Write Java data types into the above byte array
       DataOutputStream strmDataType = new DataOutputStream(strmBytes);

       byte[] record;

       for (int i = 0; i < sData.length; i++)
       {
         // Write Java data types
         strmDataType.writeBoolean(bData[i]);
         strmDataType.writeInt(iData[i]);
         strmDataType.writeUTF(sData[i]);

         // Clear any buffered data
         strmDataType.flush();

         // Get stream data into byte array and write record
         record = strmBytes.toByteArray();
         rs.addRecord(record, 0, record.length);

         // Toss any data in the internal array so writes
         // starts at beginning (of the internal array)
         strmBytes.reset();
       }

       strmBytes.close();
       strmDataType.close();

     }
     catch (Exception e)
     {
       db(e.toString());
     }
}

Next we want to read from the record store. We begin by creating the necessary input streams, then loop through each record, storing its contents in a byte array. Accessing the data input stream we read each Java primitive type from the byte array and print the associated contents to the console, as shown in Listing 6.

Listing 6. Reading a stream from the record store

public void readStream()
{
     try
     {
       // Allocate space to hold each record
       byte[] recData = new byte[50];

       // Read from the specified byte array
       ByteArrayInputStream strmBytes = new ByteArrayInputStream(recData);

       // Read Java data types from the above byte array
       DataInputStream strmDataType = new DataInputStream(strmBytes);

       for (int i = 1; i <= rs.getNumRecords(); i++)
       {
         // Get data into the byte array
         rs.getRecord(i, recData, 0);

         // Read back the data types
         System.out.println("Record #" + i);
         System.out.println("Boolean: " + strmDataType.readBoolean());
         System.out.println("Integer: " + strmDataType.readInt());
         System.out.println("String: " + strmDataType.readUTF());
         System.out.println("--------------------");

         // Reset so read starts at beginning of array
         strmBytes.reset();
       }

       strmBytes.close();
       strmDataType.close();

     }
     catch (Exception e)
     {
       db(e.toString());
     }
}

Now check out the source code for the ReadWritePrimitives MIDlet. Study it carefully, and then run the code in your WTK emulator to view the output if you like.

The console output for the ReadWritePrimitives MIDlet should look as shown in Figure 2.

Figure 2. Output of the ReadWritePrimitives MIDlet
Figure 2. Output of the ReadWritePrimitives MIDlet

The RecordEnumeration API
Up to this point we've been traversing the record store with a simple for loop. Although this technique has been sufficient to our needs so far, we'll see in this section what happens when we extend our code to include a record enumerator. One of the primary benefits of incorporating a record enumerator into your code is that it includes options for searching the record store, and for retrieving records in sorted order.

Listing 7 shows the methods for the RecordEnumeration API.

Listing 7. The RecordEnumeration API

int numRecords()
byte[] nextRecord()
int nextRecordId()
byte[] previousRecord()
int previousRecordId()
boolean hasNextElement()
boolean hasPreviousElement()
void keepUpdated(boolean keepUpdated)
boolean isKeptUpdated()
void rebuild()
void reset()
void destroy()

Incorporating enumeration into your code is as easy as creating a RecordEnumeration object from an open record store and looping through each record. Listing 8 shows how to create a RecordEnumeration. The first two parameters (which are both null in this example) specify the classes that carry out searching and/or sorting on the record store. The third parameter is the keepUpdated flag as defined in the API. When this variable is set to true the enumeration result set will be re-initialized whenever the record store is changed. Setting the parameter to false specifies the enumeration to ignore record store updates.

Listing 8. Create a RecordEnumeration

rs = RecordStore.openRecordStore(REC_STORE, true);
...

RecordEnumeration re = rs.enumerateRecords(null, null, false);
while (re.hasNextElement())
{
     // Get next record
     String str = new String(re.nextRecord());

     ...
}

Why record enumeration counts
At first the enumeration loop seems to provide essentially the same functionality as our for loop did, but in fact it offers a lot more functionality. In addition to providing a means for searching and sorting (which we'll get to momentarily), record enumeration overcomes an essential (although not immediately apparent) disadvantage of using the for loop.

From the beginning of the article, you may recall that record IDs are not re-used in a record store. Therefore, if a store has three records with IDs 1, 2, and 3 and record ID 2 is deleted, the store will consist of IDs 1 and 3. This isn't a problem until you consider how we've been looping through the record store. The code in Listing 9 will refresh your memory.

Listing 9. A typical for loop to read records

byte[] recData = new byte[5];
int len;

for (int i = 1; i <= rs.getNumRecords(); i++)
{
     // Allocate more storage if necessary
     if (rs.getRecordSize(i) > recData.length)
       recData = new byte[rs.getRecordSize(i)];

     len = rs.getRecord(i, recData, 0);
}

As you've probably figured out, we'll hit trouble with that last line of code. Although getNumRecords() will return the correct number of records (two), the values for the variable i will be 1 and 2, as returned by the for loop. But the record IDs in the store are 1 and 3. The call to rs.getRecord() will fail when i equals 2, as we no longer have a record ID with that value. It is here that record enumeration comes to our rescue, since it doesn't obtain records by ID.

Can you spare an update?
Use caution when specifying the keepUpdated parameter as true when creating an enumerator. Although the record store result set will be current with changes to the store, continuous updates may cause a performance bottleneck.

The RecordComparator API
The RecordEnumeration API provides the foundation for working with enumerations, allowing us to cycle through entries in the RMS. But it's the RecordComparator interface that lets us actually retrieve records from the record store in sorted order.

The RecordComparator API consists of one method and three pre-defined return values. The sole method in this interface accepts two parameters, and both are byte arrays. In Listing 10, you can see how these arrays represent records from the record store. The enumeration calls the compare() method passing in the contents of two records from the store. Working through the entire record store, the enumeration and our search code build a result set that is sorted based on the specified search criteria.

Listing 10. RecordComparator API

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

static int EQUIVALENT
static int FOLLOWS
static int PRECEDES

Listing 11 shows a simple class implementing the Comparator interface to sort records based on comparing the string contents of each record. Notice that the return value must be one of the pre-defined values, EQUIVALENT, PRECEDES, or FOLLOWS. The comparator class is followed by the code to create an instance of the comparator and pass this as a parameter when creating a record enumeration.

Listing 11. A Comparator class for sorting

//********************************************************
// Create comparator class for sorting
//********************************************************
public class comparator implements RecordComparator
{
     public int compare(byte[] rec1, byte[] rec2)
     {
       String str1 = new String(rec1), str2 = new String(rec2);

       int result = str1.compareTo(str2);
       if (result == 0)
         return RecordComparator.EQUIVALENT;
       else if (result < 0)
         return RecordComparator.PRECEDES;
       else
         return RecordComparator.FOLLOWS;
     }
}

...

//********************************************************
// How to access the comparator using a record enumeration
//
// Note: Variable 'rs' is created outside the scope of
//       this method
//********************************************************

// Create a new comparator for sorting
comparator comp = new comparator();

// Reference the comparator when creating the result set
RecordEnumeration re = rs.enumerateRecords(null, comp, false);

// Retrieve each record in sorted order
while (re.hasNextElement())
{
     String str = new String(re.nextRecord());
     ...
}

Sorting multiple data types
Clearly, sorting records that consist solely of string data is a piece of cake. Working with a record store that contains multiple data types in each record presents a little more of a challenge, however. For example, refer back to Listing 4. In this example we wrote three primitive Java data types into each record, a boolean, integer, and string. So, how do we go about sorting based on one of the specific data types? In order to accomplish this our comparator function will need to extract the appropriate field to compare on, perform the comparison, and return the appropriate value (EQUIVALENT, PRECEDES, or FOLLOWS) to the enumerator.

To better understand how all this works, let's write a MIDlet that sorts based on an integer. We'll start by updating the writeTestData() method from Listing 4 so that each record in the record store contains both string data and an integer, as shown below:

Listing 12. Java primitive data types for sorting

public void writeTestData()
{
     String[] strings = {"Java", "J2ME", "C"};
     int[] integers = {2, 1, 3};

     writeStream(strings, integers);
}

The output of the IntegerSort MIDlet is shown in Figure 3. Notice that the comparator function (which we'll write shortly) returns records that have been sorted based on the integer value stored within each record.

Figure 3. Records sorted by integer
Figure 3. Records sorted by integer

Building a comparator
For the most part, the code for the IntegerSort MIDlet (see the complete source code) won't differ much from that of our previous MIDlets. The biggest change is the addition of a comparator class (Listing 13) and the methods for extracting the appropriate fields and performing the actual sorting. Following is the ComparatorInteger class that handles all the details for the IntegerSort MIDlet.

Listing 13. The ComparatorInteger class

/*--------------------------------------------------
* Compares two integers to determine sort order
* Each record passed in contains multiple Java data
* types - use only the integer data for sorting
*-------------------------------------------------*/
class ComparatorInteger implements RecordComparator
{
     private byte[] recData = new byte[10];

     // Read from a specified byte array
     private ByteArrayInputStream strmBytes = null;

     // Read Java data types from the above byte array
     private DataInputStream strmDataType = null;

     public void compareIntClose()
     {
       try
       {
         if (strmBytes != null)
           strmBytes.close();
         if (strmDataType != null)
           strmDataType.close();
       }
       catch (Exception e)
       {}
     }

     public int compare(byte[] rec1, byte[] rec2)
     {
       int x1, x2;

       try
       {
         // If either record is larger than our buffer, reallocate
         int maxsize = Math.max(rec1.length, rec2.length);
         if (maxsize > recData.length)
           recData = new byte[maxsize];

         // Read record #1
         // We want the integer from the record, which is
         // the second "field" thus we must read the
         // String first to get to the integer value
         strmBytes = new ByteArrayInputStream(rec1);
         strmDataType = new DataInputStream(strmBytes);
         strmDataType.readUTF();       // Read string
         x1 = strmDataType.readInt();  // Read integer

         // Read record #2
         strmBytes = new ByteArrayInputStream(rec2);
         strmDataType = new DataInputStream(strmBytes);
         strmDataType.readUTF();       // Read string
         x2 = strmDataType.readInt();  // Read integer

         // Compare record #1 and #2
         if (x1 == x2)
           return RecordComparator.EQUIVALENT;
         else if (x1 < x2)
           return RecordComparator.PRECEDES;
         else
           return RecordComparator.FOLLOWS;

       }
       catch (Exception e)
       {
         return RecordComparator.EQUIVALENT;
       }
     }

Pay close attention to the code for reading the Java primitives. We need to retrieve the integer value from each record. However, this value is the second "field" stored in each record. Therefore, we simply read the string (UTF) value and toss it aside. The second read stores the integer value in a local variable (x1 or x2). These values are then compared to determine the proper sort order.

Listing 14. Reading and sorting Java primitives

// Read record #1
...
strmDataType.readUTF();       // Read string
x1 = strmDataType.readInt();  // Read integer

// Read record #2
...
strmDataType.readUTF();       // Read string
x2 = strmDataType.readInt();  // Read integer

// Compare record #1 and #2
if (x1 == x2)
     return RecordComparator.EQUIVALENT;
else if (x1 < x2)
     return RecordComparator.PRECEDES;
else
     return RecordComparator.FOLLOWS;

Building an enumerator
With the comparator code written, we next create an enumerator, referencing an instance of the ComparatorInteger class. The enumerator will create a result set of records from the record store, using the comparator as the sorting algorithm. Listing 15 is a partial listing of the readStream() method which creates the comparator as well as the enumerator, and loops through the result set displaying record contents on the console.

Listing 15. readStream() method

public void readStream()
{
    ...

     if (rs.getNumRecords() > 0)
     {
       // Create instance of the comparator
       ComparatorInteger comp = new ComparatorInteger();

       // Create enumerator, referencing the comparator
       RecordEnumeration re = rs.enumerateRecords(null, comp, false);

       // Loop through all elements in the result set
       int i = 1;
       while (re.hasNextElement())
       {
         // Get data into the byte array
         rs.getRecord(re.nextRecordId(), recData, 0);

         // Read back the data types
         System.out.println("Record #" + i++);

         System.out.println("String: " + strmDataType.readUTF());
         System.out.println("Integer: " + strmDataType.readInt());
         System.out.println("--------------------");

         // Reset so read starts at beginning of array
         strmBytes.reset();
       }

     ...

}

The RecordFilter API
Sorting with a comparator is one option when working with an enumerator, searching with a filter is the other. A minor difference between a comparator and a filter is that a comparator returns the entire record store in sorted order, whereas a filter returns only those records that match a specified criteria. If you apply both a comparator and a filter, records that match the search criteria will be returned in sorted order.

Like the RecordComparator, the RecordFilter is implemented by the addition of a single method, matches(), to the enumerator code. The enumerator calls thematches() method for each record in the store. Based on the boolean return value, the record either becomes a member of the result set or is tossed aside as a record that does not meet the search criteria.

Listing 16. The RecordFilter API's matches() method

boolean matches(byte[] candidate)

Building a filter
Listing 17 shows a class that implements the RecordFilter interface. Notice that the search string is specified as a parameter to the constructor SearchFilter. The string is saved in a private variable so we have access to the string when the enumerator calls the matches() method to create the result set. For this example, the search string is also converted to lowercase, resulting in a search that is not case-sensitive.

Listing 17. Building a RecordFilter

//********************************************************
// Create filter class for searching
//********************************************************
class SearchFilter implements RecordFilter
{
     private String searchText = null;

     public SearchFilter(String searchText)
     {
       // Text to find
       this.searchText = searchText.toLowerCase();
     }

     public boolean matches(byte[] candidate)
     {
       String str = new String(candidate).toLowerCase();

       // Does the text exist?
       if (searchText != null && str.indexOf(searchText) != -1)
         return true;
       else
         return false;
     }
}

...

//********************************************************
// How to access the filter using a record enumeration
//
// Note: Variable 'rs' is created outside the scope of
//       this method
//********************************************************
// Create search filter
SearchFilter search = new SearchFilter("abc");

// Reference filter when creating the result set
RecordEnumeration re = rs.enumerateRecords(search, null, false);

// If there is at least one record in result set, a match was found
if (re.numRecords() > 0)
{
     // At least one record in the result set, do something here...
     ...
}

Note that the actual code that performs the search in this example is quite trivial. Using the Java string method indexOf(), we look for the specified search string and return a boolean value indicating success or failure.

The StringSearch MIDlet
We'll build one last MIDlet to illustrate what you've learned so far. (Here's the complete source for the StringSearch MIDlet.) In addition to allowing us to search the RMS for records, this MIDlet beefs up the user interface we've worked with so far. Instead of using only the console for output, we'll display a TextField component (which you should recall from the tutorial series!) that will prompt the user for a text string. When requested, we'll search the record store for the string. Any and all matches will be appended on the display, showing the search results.

Listing 18 shows the search strings that will be written into the record store.

Listing 18. Entries to the record store

public void writeTestData()
{
     String[] strs = {
                     "I think this would be a good time for a beer. (FDR)",
                     "I'll make it a felony to drink small beer. (Shakespeare)",
                     "They who drink beer will think beer. (Washington Irving)",
                     "I would give all my fame for a pot of ale. (Shakespeare)"};
     writeRecords(strs);
}

Figure 4 shows the MIDlet with two different search results.

Figure 4. The record search results
Figure 4. The record search results

Creating the user interface consists of allocating a Form, a TextField, and two Commands -- one to search the record store and one to exit the MIDlet, as shown in Listing 19.

Listing 19. Create the user interface components

...
// Define textfield, stringItem and commands
tfFind = new TextField("Find", "", 12, TextField.ANY);
cmExit = new Command("Exit", Command.EXIT, 1);
cmFind = new Command("Find", Command.SCREEN, 2);

// Create the form, add commands
fmMain = new Form("Record Search");
fmMain.addCommand(cmExit);
fmMain.addCommand(cmFind);

// Append textfield and stringItem to form
fmMain.append(tfFind);
...

Event processing
Once the StringSearch MIDlet is active, all events will be processed inside the commandAction() method. When the command cmFind is requested, we call searchRecordStore() to begin the process of searching. This consists of allocating an instance of our SearchFilter() class and associating this instance with a record enumeration object. When the enumeration object is allocated, it will call our search function to search the record store. Any records that match the search string will become part of the enumeration result set.

Listing 20 shows the code to process events and search records for a match against a user-entered text string. Notice the reference to tfFind.getString() when creating the search filter. This call obtains the user-entered search string from the textfield. The string is stored inside the SearchFilter class in the variable searchText. When the enumerator calls the method matches() -- once for each record in the record store -- it will search the active record for a string that matches searchText.

Listing 20. Processing a user event

public void commandAction(Command c, Displayable s)
{
     if (c == cmFind)
     {
       searchRecordStore();
     }
     else if (c == cmExit)
     {
       destroyApp(false);
       notifyDestroyed();
     }
}

...

//********************************************************
// Search the record store
//********************************************************
private void searchRecordStore()
{
     try
     {
       // Record store is not empty
       if (rs.getNumRecords() > 0)
       {
         // Setup the search filter with the user requested text
         SearchFilter search = new SearchFilter(tfFind.getString());

         RecordEnumeration re = rs.enumerateRecords(search, null, false);

         // Remove any previous record entries displayed on the form
         clearForm();

         // A match was found using the filter
         if (re.numRecords() > 0)
         {
           // Append all records found onto the form
           while (re.hasNextElement())
             fmMain.append(new String(re.nextRecord()));
         }

         re.destroy();   // Release enumerator
       }
     }
     catch (Exception e)
     {
       db(e.toString());
     }
}

...

//********************************************************
// Called for each record when creating the enumerator.
// Checks to see if the record contains text that
// matches the text string entered by the user.
//********************************************************
class SearchFilter implements RecordFilter
{
     private String searchText = null;

     public SearchFilter(String searchText)
     {
       // Text to find
       this.searchText = searchText.toLowerCase();
     }

     public boolean matches(byte[] candidate)
     {
       String str = new String(candidate).toLowerCase();

       // Look for text
       if (searchText != null && str.indexOf(searchText) != -1)
         return true;
       else
         return false;
     }
}

The RecordListener API
The RecordListener interface is the last API we'll discuss, but not the least important one. Implementing a RecordListener in your code ensures that you will be notified of changes, additions, or deletions to the record store.

Here are the basic steps for working with a RecordListener:

  • Open (create) a record store.
  • Create a new listener
  • Implement each method in the RecordListener interface

For each method in the RecordListener API, the parameters passed in are the same: a reference to the record store upon which the change has occurred and the record ID that was affected. Listing 21 shows the methods for the RecordListener API.

Listing 21. The RecordListener API

void recordAdded(RecordStore recordStore, int recordId)
void recordChanged(RecordStore recordStore, int recordId)
void recordDeleted(RecordStore recordStore, int recordId)

The short code block in Listing 22 demonstrates how you can display a message to the console upon any adding, deleting, or changing a record in the RMS.

Listing 22. Creating a record listener

// Open record store
rs = RecordStore.openRecordStore(REC_STORE, true);
...

// Using handle to open record store, create a listener
rs.addRecordListener(new DemoRecordListener());

...

//********************************************************
// Listener to process updates to the record store
//********************************************************
class DemoRecordListener implements RecordListener
{
     public void recordAdded(RecordStore recordStore, int recordId)
     {
       System.out.println("Record added");
     }

     public void recordDeleted(RecordStore recordStore, int recordId)
     {
      		 System.out.println("Record deleted");
     }

     public void recordChanged(RecordStore recordStore, int recordId)
     {
      		 System.out.println("Record changed");
     }

Conclusion
In this third installment in the J2ME 101 series, you've learned how to create and manage data records in your MIDP applications. The convenience methods introduced at the beginning of the article will let you write and read records into the record store. The RecordEnumeration class enables you to move among all records in the RMS. When combined with the RecordComparator it can return records in sorted order, and when combined with a RecordFilter it can search for specific records. If you combine all three -- the enumerator, comparator, and filter -- you can search for specific records and return them in sorted order. The RecordListener, the last-but-not-least API we discussed, can be used to set up event notification for your applications.

We'll close this series in a couple of weeks, with a comprehensive introduction to network support in MIDP, so stay tuned!

Resources

  • Download the source code for the MIDlets in this article.

  • Download Sun Microsystems's J2ME Wireless Toolkit to compile the code samples.

  • Not sure how to load that thing? The author's tutorial, "MIDletdevelopment with the Wireless Toolkit" (developerWorks, March 2003) will get you started.

  • If you missed the J2ME 101 series, start now to get a leg up on :
    • Part 1 provides an introduction to the MIDP high-level interface.
    • Part 2 details MIDP's low-level interface.
    • Part 4 examines the Generic Connection Framework.

  • For a more DIY (do-it-yourself) intro to MIDP, turn to java.sun.com's MIDP home page.

  • Soma Ghosh's "J2ME record management store" (developerWorks, May 2002) is another good introduction to RMS.

  • Learn a little more about the inner RMS, with the two-part series on MicroDevNet, also by the author.

  • The WebSphere Micro Environment provides an end-to-end solution connecting cellular phones, PDAs, and other pervasive devices to e-business.

  • The alphaWorks Web Services Toolkit for Mobile Devices provides tools and a runtime environments for developing applications that use Web services on small mobile devices, gateway devices, and intelligent controllers.

  • Core J2ME (Prentice Hall PTR, 2002) by John W. Muchow is a comprehensive guide to J2ME development. You can also visit the Core J2ME Web site for additional articles, tutorials and developer resources.

  • The developerWorks Wireless zone offers a wealth of technical content on pervasive computing.

  • You'll find hundreds of articles about every aspect of Java programming in the developerWorks Java technology zone.

  • Also see the Java technology zone tutorials page for a complete listing of free Java-related tutorials from developerWorks.

About the author
John Muchow, author of Core J2ME technology and MIDP, is a freelance technical writer and developer, with extensive experience in J2ME, Java, JSP, C and ColdFusion. Visit Core J2ME for additional source code, articles, and developer resources. Send John e-mail for additional information about writing or software development projects.


203 KBe-mail it!

What do you think of this document?
Killer! (5) Good stuff (4) So-so; not bad (3) Needs work (2) Lame! (1)

Comments?



developerWorks > Java technology
developerWorks
  About IBM  |  Privacy  |  Terms of use  |  Contact