Developers
Local Navigation
Richard Evers, Editor
Persistently trying to resolve issues, persisting to an old age, and the persistent odor of a light perfume. These can all be good things. Committing facts to memory, writing a shopping list, taking notes in class, sending email, saving documents to disk, and storing information within a database are all good examples of persistence.
In the wireless world of BlackBerry development, persistence can be found when sending and receiving email; making browser requests; pushing and pulling data; syncing and adjusting calendar, task list, contacts, and notepad data; installing applications over the air; and storing data programmatically.
In this article, we will address the programmatic storage of data on the BlackBerry handheld.
Topics within this section include:
- Considering the facts
- Two ways to do it
- Code signing
- BlackBerry PersistentStore & PersistentObject
- MIDP Record Stores
- Summary
Considering the facts
Creating an application for the BlackBerry handheld requires a developer to consider many factors not present in a conventional system, such as:
- Persistent store objects live in a finite amount of flash memory that is shared with the operating system, applications for BlackBerry, third-party applications, and user data.
- The data transfer rates to and from flash memory is slower than conventional memory (RAM).
- Data transmission speeds across wireless networks are far lower than across wireline networks.
- Reading and writing to flash memory forces the CPU to work harder, which in turn compromises battery life.
- Physical representations of persistent data are restricted to byte arrays for MIDlets (and Java objects when writing specifically for the BlackBerry API).
Two ways to do it
There are two ways to programmatically store data on a Java-based BlackBerry handheld:
- MIDP record stores
- The BlackBerry Persistence API set
MIDP record stores should be used when writing applications that will work on all J2ME-enabled devices. MIDP record stores have a lot of methods available to manipulate them, and are relatively easy to work with.
The most useful feature is the ability to read, insert, delete, and update individual records within a store of records without having to load the entire store into memory first. The down side is that you can only store byte arrays, cannot retain more than 64 KB of data within a record store, and automatically provide data visibility to MIDlets within the same MIDlet suite.
The signed BlackBerry persistence model can be used to develop applications that take full advantage of the BlackBerry architecture. Size restrictions are largely removed, data visibility is set by the application, and objects (including custom objects) can be saved to persistent store. Persistently stored data can also be backed up and restored through the use of the signed synchronization API in the net.rim.device.api.synchronization package.
Note that PersistentStore is a lightweight database solution. You can serialize an object of any type to persistent store, but cannot selectively update or locate elements. You must load the entire object into memory, alter as required, and then commit the entire object back to persistent store.
Code signing
The BlackBerry persistence model requires the use of signed APIs. For more information, please review Jonathan Nobels's article, Give Me A Sign, which has been published in this issue.
BlackBerry PersistentStore & PersistentObject
Serialization of data is straightforward with the BlackBerry API.
Create/Open
Call PersistentStore.getPersistentObject(long_key) to retrieve a reference to an existing PersistentObject, or to create a new one if it does not exist. Use a static constructor so that only one PersistentObject is created the first time that an object of this class is created. Each time a process starts, the static block will be run again.
public class MyStore implements KeyListener, TrackwheelListener {
private static PersistentObject store;
static {
// key hash of com.rim.bbdj.mystore
store = PersistentStore.getPersistentObject(0xa406067aeb8ca6ebL);
}
Note that "long_ key" must be a unique long value. The easiest way to create a unique value is to create a hash of your fully qualified package name. If you use more than one persistent store object within your application, append a descriptive table identifier to your package name before deriving the hash. The steps required to create a hash value from within the BlackBerry IDE are:
- Type a string value, such as com.rim.bbdj.mystore
- Highlight the entire string
- Right-click then click "Convert 'com.rim.bbdj.mystore' to long"
The long value appears (0xa406067aeb8ca6ebL). Make sure to include a comment in your code to indicate the string used to generate the long key.
Store
Use PersistentObject.setContents(Object obj) then PersistentObject.commit() within a synchronized block to store data.
private MenuItem saveItem = new MenuItem("Save", 110, 10) {
public void run() {
String username = new String("Tiberius");
String password = new String("IGrokSpock");
String[] userinfo = {username, password};
synchronized(store) {
store.setContents(userinfo);
store.commit();
}
}
};
Retrieve
Use PersistentObject.getContents() within a synchronized block to retrieve data.
private MenuItem getItem = new MenuItem("GetItem", 110, 11) {
public void run() {
synchronized(store) {
if(store.getContents() == null)
System.out.println("Error");
else
String[] currentinfo =
(String[])store.getContents();
}
}
};
Delete
Use PersistentStore.destroyPersistentObject( long id ) to delete the database.
PersistentStore.destroyPersistentObject(0xa406067aeb8ca6ebL);
Custom Persistent Objects
Custom objects can be stored persistently. The process is basically the same when creating/opening, storing, retrieving and deleting a persistent store object.
There are two main differences. First, you need to work with anything that extends Object. Second, the class of the object to be saved must implement the Persistable interface.
The main differences in sequence:
1. Create a Vector object to store multiple objects
private static Vector data;
2. Create a PersistentObject database
private static PersistentObject store;
3. Initialize the database to store a Vector
static {
store = PersistentStore.getPersistentObject(0xa406067aeb8ca6ebL);
synchronized (store) {
if (store.getContents() == null) {
store.setContents(new Vector());
store.commit();
}
}
data = new Vector();
data = (Vector)store.getContents();
}
4. Create a Persistable class to handle the Vector data
private static final class RestaurantInfo implements Persistable {
//data
private Vector elements;
//fields
public static final int NAME = 0;
public static final int ADDRESS = 1;
public static final int PHONE = 2;
public static final int SPECIALTY = 3;
public RestaurantInfo() {
elements = new Vector(4);
for ( int i=0; i<elements.capacity(); ++i)
elements.addElement(new String(""));
}
public String getElement(int id) {
return (String)elements.elementAt(id);
}
public void setElement(int id, String value) {
elements.setElementAt(value, id);
}
}
MIDP Record Stores
MIDP Record Management System (RMS) provides developers with the ability to create persistent store databases, then selectively add, update and delete individual rows of byte array data within the database.
Multiple MIDlets within a MIDlet suite, or threads within a MIDlet, can open databases, and read/write records within a persistent store database. This can be performed without clash because all record store operations are atomic, synchronous and serialized. To ensure data integrity, locking of the entire RecordStore is employed when reading/writing individual records within the store. While table locking is unacceptable with conventional relational database systems, it was deemed acceptable on wireless devices as the overhead for memory/storage and processing to maintain row-level locks would be far too excessive.
MIDP relies on the javax.microedition.rms.RecordStore class and four interfaces for most operations:
- RecordComparator
- RecordEnumerator
- RecordFilter
- RecordListener
Each MIDlet suite has its own separate name space for record stores. This means that MIDlets can access any record store within their MIDlet suite.
Create/Open
Create a new RecordStore database by calling RecordStore.openRecordStore(), passing the name of the database, and a Boolean flag set to 'true' to create the database if it doesn't exist.
String name = "myDb";
RecordStore rs = RecordStore.openRecordStore( name, true );
Best practice is to iterate through all RecordStores in the MIDlet suite to determine if a naming clash exists before creating or opening a database. This can be accomplished as follows:
String [] stores = RecordStore.listRecordStores();
if ( stores!= null ) {
for ( int element = 0;
element < stores.length;
element++ ) {
if ( stores[element].equals(name)) {
// name is already in use
}
}
}
It is best to check before creating a new database for the following reasons:
- Any MIDlet within a MIDlet suite can create databases that are shared within the suite
- RecordStore names are limited to 32 Unicode case-sensitive characters
- No guidelines are in place to construct filenames
Store
There are two ways to store record data.
The first method adds a new record to a RecordStore database:
// addRecord(byte[], byte[]);
int recordId = rs.addRecord(data, 0, data.length);
Pass a byte [] buffer populated with the data to write, the offset into the buffer where the data starts, and the number of bytes to write. The example uses data.length, which assumes that the full buffer is being used. This method returns the sequential record identifier assigned to the new record.
The second method updates an existing record within a RecordStore database:
// setRecord( int, byte[], int, int );
rs.setRecord(recordId, data, 0, data.length);
Pass the record identifier, a byte [] buffer populated with the data to write, the offset into the buffer where the data starts, and the number of bytes to write.
Retrieve
There are two ways to retrieve a RecordStore record.
The easiest is to simply pass the record identifier you want:
// getRecord( int );
byte [] data = rs.getRecord(recordId);
The second method is a little more powerful because it allows you to selectively append data to the byte [] buffer:
// int getRecord( int, byte[], int );
int bytes_copied = getRecord(recordId, data, offset);
Pass the record identifier, a buffer to hold the byte [] data, and the starting offset into the buffer to write data. The number of bytes retrieved into the passed byte [] buffer (data) is returned.
Delete
Delete an individual record via:
// deleteRecord( int );
rs.deleteRecord(recordId);
Delete the entire RecordStore database via:
// deleteRecordStore( String );
rs.deleteRecordStore(recordStoreName);
Close
if ( rs != null )
rs.closeRecordStore();
Row iteration
RecordStore.enumerateRecords() is used to step through the rows within an active RecordStore database. The returned RecordEnumeration object can be used to work forward or backward through a record set as defined when first calling enumerateRecords. This process can be made to simply walk through all rows without regard to sequence, or can be enhanced to track row changes, filter results or custom-sort underlying rows.
Methods available within the RecordEnumeration interface include:
- public byte[] nextRecord();
- public byte[] previousRecord();
- public int nextRecordId();
- public int previousRecordId();
- public boolean hasNextElement();
- public boolean hasPreviousElement();
- public int numRecords();
- public void keepUpdated( boolean keepUpdated );
- public boolean isKeptUpdated();
- public void reset();
- public void rebuild();
- public void destroy();
The following code can be used to access all rows within a RecordStore database in sequence. Note that the parameters passed to enumerateRecords() disable RecordFilter and RecordComparator, and also disable automatic updates to the underlying rows if any external changes occur during processing:
// open, but not create, database
RecordStore rs = RecordStore.openRecordStore( "myStoreDB", false );
if (rs != null ) {
RecordEnumeration dataset = null;
try {
// parm 1: RecordFilter [ disabled ]
// parm 2: RecordComparator [ disabled ]
// parm 3: track changes [ disabled ]
// RecordEnumeration enumerateRecords
// (RecordFilter, RecordComparator,
// boolean);
dataset = rs.enumerateRecords( null, null, false );
while(dataset.hasMoreElements()) {
int recordId =dataset.getNextRecordId();
// row identifier is valid
if ( recordId )
byte [] data = rs.getRecord(recordId);
}
}
catch( RecordStoreException except ) {
}
finally {
dataset.destroy();
}
rs.closeRecordStore();
}
Note: RecordEnumeration sets can be traversed forward and backward.
For forward navigation, use RecordEnumeration. getNextRecordId() in combination with RecordStore. getNextRecord() to get the next row, and RecordEnumeration.hasNextElement() to determine if additional rows are available.
For backward navigation, use RecordEnumeration. getPreviousRecordId and RecordStore.getPreviousRecord() to get the previous row, and RecordEnumeration. hasPreviousElement() to determine if additional rows are available.
Call RecordEnumeration.reset() at any time to change the direction, and reset access at the start.
RecordStore.enumerateRecords Parameters
Parameter 1: RecordFilter
Passing a RecordFilter object to enumerateRecords() will pass the byte [] data within all processed rows to the matches() method of the RecordFilter object before allowing processing to occur.
The RecordFilter interface contains a single method as shown below:
public interface RecordFilter {
public boolean matches( byte[] recordData );
}
A sample implementation could work as follows:
dataset = rs.enumerateRecords( new DSFilter(), null, false );
...
public class DSFilter implements RecordFilter{
public boolean matches( byte[] recordData ){
boolean ret = false;
if (recordData[0] != 0
&& recordData.length > 0)
ret = true;
return ret;
}
}
Parameter 2: RecordComparator
Pass a RecordComparator object to enumerateRecords() to sort rows. It contains a single method as shown below:
public interface RecordComparator {
public int compare(byte[] rec1,byte[] rec2);
public int EQUIVALENT = 0;
public int FOLLOWS = 1;
public int PRECEDES = -1;
}
A sample implementation could work as follows:
dataset = rs.enumerateRecords( null, new DSCompare(),false );
...
public class DSCompare implements RecordComparator {
public int compare(byte[] rec1, byte[] rec2){
String srec1 = new String(rec1);
String srec2 = new String(rec2);
return (srec1.compareTo(srec2));
}
}
Note that there will be a speed penalty when using RecordComparator, especially when the third parameter, keepUpdate, is set to true. In this situation, the entire data set will automatically be resorted after the RecordEnumeration index has been rebuilt.
Parameter 3: keepUpdated
The third parameter to enumerateRecords() should be set to 'true' only if there is a risk that some rows will be externally changed, added or deleted during navigation. In this situation, the RecordEnumeration will become a listener of the RecordStore and react to record additions and deletions by recreating its internal index. Do not use this feature unless absolutely warranted, as there will be a performance hit every time the index is rebuilt due to a change to the RecordStore.
Summary
If you do not require the cross-platform capability offered by MIDP record stores, use the BlackBerry persistence API set to take full advantage of the BlackBerry handheld architecture.