Chapter 19. Be Persistent: MIDP Data Storage

So, now we've got some data we'd like to store persistently on our J2ME device—the images for the car's weapons. By storing these directly on the device, we don't have to download them every time the MIDlet fires up. Fine, but how do you do it across so many diverse devices, most of which are without hard drives or even file systems?

As with most things in the J2ME scheme of things, data storage is in the mix, but it's rather different than you might be used to, and requires a little limited-device common-sense to use effectively and efficiently.

To take advantage of J2ME's data access capabilities, we'll need to make use of the classes and interfaces in a core MIDP package we haven't yet touched, the javax.microedition.rms package. (The “rms” acronym stands for Record Management System.) This package's RecordStore class is our key to data storage and handling.

RecordStore Overview

Before we dive into coding, let's take a look at RecordStore's quirks and limitations. Here's what the class javadocs say:

“…A record store consists of a collection of records which will remain persistent across multiple invocations of the MIDlet. The platform is responsible for making its best effort to maintain the integrity of the MIDlet's record stores throughout the normal use of the platform, including reboots, battery changes, etc.”

It's worth paying attention to that last bit—the platform will make its best effort” to keep persistent data safe and sound. For the most part, we can assume that any given device will follow through, but we'll really need to account for the possibility that the storage might get wiped, and that we'll need to rebuild it if it's not found.

Working with a RecordStore is rather like working with a database, or with a RandomAccessFile from J2SE's java.io package. You create and access a RecordStore using a unique String ID (up to 32 Unicode characters long), and you can then add, read, set, or delete individual records of data within it. There are a bevy of new exceptions to handle with a RecordStore, which we'll touch on later.

A RecordStore is associated only with the MIDlet suite that created it. Any RecordStore created by any MIDlet within a suite is available to all other MIDlets in the same suite. However, you can't access a RecordStore in another suite.

When you add a new piece of data to a RecordStore, it's added via the addRecord() method, which returns a unique recordId int primitive that can be used to access the same record later on. The recordId is guaranteed to start at 1 for the first record of a new RecordStore, and will increment by one for each record subsequently added to it.

This recordId number increments absolutely, regardless of any record deletions. In other words, if you add three records to a new RecordStore upon creating it, their recordIds will be 1, 2, and 3 in the order in which they were added. If you then delete the second record and add an entirely new one, the recordIds will be 1, 3, and 4. Bearing this in mind, we'll use this behavior to our advantage to keep track of our images consistently—if perhaps inflexibly—although we'll hint at how to handle records in a more arbitrary fashion toward the end of the chapter (using a RecordEnumeration).

Data is stored and retrieved to and from individual records only as byte arrays, and so must be packed into byte arrays for storage, then unpacked to rebuild and use as needed by the MIDlet. We'll build some methods for doing this simply with our data.

The amount of RecordStore storage space depends wholly upon the device, and is generally shared with the MIDlet storage. In other words, don't believe you've got megabytes of memory to dump data into. You might only have some tens of kilobytes, if even that. Keep only what you need, and clear out what you don't. So, when the car's images need to change, we'll completely replace the older data with the new stuff.

Likewise, there's no guarantee that data access speed will be anything remarkable. Don't worry too much: accessing an individual record will likely only take a few milliseconds, but it may very well be much slower than using normal Java variable objects on the VM's memory heap. Furthermore, for many devices (for example, Palm Pilots), writing to memory storage is often considerably slower than reading from it, usually because of well-considered memory-locking and security.

The key here for us is that the data is persistent across “multiple invocations of the MIDlet.” This is RecordStore's best use, instead of storing and retrieving data on the fly during a MIDlet's normal operation. The rule of thumb is that for any data which should be persistent, it should be moved from normal runtime variables into the RecordStore before the MIDlet shuts down. The MIDlet should then unpack the record data into those variables when it starts up again.

Finally, a RecordStore is thread-safe. It is synchronized so that only a single running thread can access a record at a time. Remember, it's still up to the developer to make sure that, if there are multiple threads in a given MIDlet that may potentially access the same record, that they do so with some intelligence. In other words, if one thread adds records to a store and a second thread only retrieves records, the second one should be smart enough not to retrieve a particular record until the first thread has actually added it.

Those are the basic ups and downs of RecordStores, so now let's see what they can do!

RecordStore in Practice

A RecordStore isn't built via a constructor like most other objects; rather, it's effectively requested from the client device via a static method call:

RecordStore openRecordStore(String recordName, boolean createIfNecessary)

If a RecordStore specified by the recordName argument exists on the device, it's found and returned. If it doesn't exist and the createIfNecessary Boolean argument is true, a new RecordStore is built and returned. If an exception is thrown, it might not exist, or the device might not be able to create any new RecordStores (possibly not enough spare memory). Other useful RecordStore methods available are described in the following sections.

addRecord()

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

This is the single method used to add new data to a RecordStore. The data must be handled as a byte array, and will be stored in the new record as such. You can specify the offset index in the data array and the number of bytes to actually store. If successfully added (without generating an exception), the method will return the recordId int for the new record. Because this is a write operation, the RecordStore is blocked to all other accesses until the record is fully written and added successfully.

getRecord()

getRecord(int recordId)

This returns a copy of the byte array stored at the location specified by recordId. Note that it indeed returns a copy; changing the copy won't alter the record in any way. If the recordId doesn't exist in the record, this will throw an InvalidRecordIDException.

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

Instead of returning the byte array, this method inserts a copy of the record's array into the supplied buffer array, beginning at the offset index in the buffer. It then returns an int representing the number of bytes copied into the buffer. Note that this could result in an ArrayIndexOutOfBounds exception if the buffer is too small to accommodate the record's array from the offset index.

setRecord()

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

This is used similarly to the addRecord() method, but it wholly replaces the record at the recordId specified. The recordId remains in the RecordStore, but now points to the new data. The offset can be used to point at a starting index in the newData array, along with numBytes, to indicate the range of newData's indices that will actually be stored as its own array in the record.

deleteRecord()

deleteRecord(int recordId)

This deletes the record in the store associated with the recordId. The recordId value is effectively gone from the RecordStore and will not be reused.

getLastModified()

long getLastModified()

This returns the last time the record store was modified.

getNextRecordID()

int getNextRecordID()

Returns the recordId that would be assigned to the next record added. This is useful when we need to check up on the recordId status without actually adding a record.

getNumRecords()

int getNumRecords()

Returns the number of records currently in the RecordStore. This has no relation to the recordId status.

getSize()

getSize()

Returns the total size, in bytes, used by the RecordStore.

getSizeAvailable()

getSizeAvailable()

This returns the total bytes still available in the device's storage for the RecordStore to use. For some games and applications, this will be a key method to confirm how much to rely upon the local device storage, and to scale that reliance appropriately.

deleteRecordStore()

deleteRecordStore(String recordName)

This is a static class method, and it completely removes a RecordStore from the device's persistent memory storage. This is unrecoverable, and any data remaining in the RecordStore is lost. If you need to reset a RecordStore and start from scratch (at recordId == 1), you'll need to delete it and create a new one.

EnumerateRecords()

RecordEnumeration enumerateRecords(RecordFilter filter, RecordComparator comparator,
EnumerateRecords() boolean keepUpdated)

We'll look a little at enumerating RecordStores later in the chapter. Enumeration allows for some greater flexibility in record handling, at the cost of some efficiency.

RecordStore Exceptions

These are the new exceptions we can catch when dealing with RecordStores. Just about every method shown so far will need to be contained in a try-catch block, and can potentially throw at least one of the following:

  • RecordStoreExeption—. This is the most generic of the RecordStore's exceptions, and is the super class of most of the others.

  • RecordStoreNotFoundException—. This is thrown whenever the RecordStore requested isn't found in the device's storage.

  • RecordStoreNotOpenExceptionThis is thrown whenever an attempt at access (read/write/whatever) is executed on a RecordStore that hasn't been properly opened for use by the MIDlet.

  • RecordStoreFullException—. This can be a big one, the RecordStore has no memory space left available to store new records.

  • InvalidRecordIDException—. One of these is thrown whenever a recordId is used that doesn't exist in the RecordStore. This can actually be useful to catch and discard while iterating through a RecordStore up to its getNextRecordID() value.

The Game's New Methods

Whoops! Let's just hang on a second; there are a couple more J2ME caveats to consider before we start implementing anything, this time regarding the Image class. This isn't your dad's old reliable J2SE Image class; it's rather more restrictive and we'll have to hurdle the following issues:

  • Once an Image, always an Image. There are no PixelGrabbers or other useful classes to pull the raw image data from an Image object.

  • You can't just point the Image class at a likely URL and hope to pop out anything better than a null. To get our remote image files on the server downloaded and looking good on the device, we'll need to access them as binaries. In other words, as byte arrays.

What this means for us is that we're going to be keeping two versions of the Images in memory. One version will be the binary source byte array, and the other will be the resulting displayable Image.

We're going to define the methods in a generic form, so that they're not hard-coded only to handling the weapon Images and data.

Here they are, in brief:

  • public byte[][] getImageDataFromStore()—. If the RecordStore is present, this will rebuild each weapon Image's source byte array from the records found and return a two-dimensional byte array for all Images found. If the method returns null, it couldn't find its RecordStore (or experienced some other device issue), and the MIDlet will have to download the images over the Internet.

  • public void storeImageData(byte[][] imageData)—. Before the MIDlet shuts down, this method will delete the current RecordStore (if present), build a new RecordStore, and pack the supplied byte arrays into records. If the method can't create the RecordStore, or there isn't enough room for the data, the method will simply return without issue. This means that the MIDlet will load the images over the Internet the next time it runs.

    As stated previously, a RecordStore returns a predictable ordering of recordId ints when adding records to it, starting at recordId == 1 for the very first record added, and incrementing by one for each subsequent record added. We'll use this behavior to keep our Images in the same order in the RecordStore as they will be in the Images array.

    The static ints already a part of the Weapon class will come in handy to properly index and ID the records Weapon.FLAME (== 0) and Weapon.OIL (== 1).

  • protected boolean removeImageStore()—. The last method we'll need will simply delete the RecordStore completely. Since it's possible we might only partially build the RecordStore before running into problems, it's only proper to make sure it's removed from the device's storage space, and not just leave it littering up the MIDlet neighborhood.

The MIDlet will also need some new member variables:

private static final String IMAGE_STORE = "CarDB";
private Image[] weaponImages;
private byte[][] weaponImageSource;
private RecordStore imageDB;

The Image array, weaponImages, will be used to handle the weapon Image objects on the client and the IMAGE_STORE string will be used to uniquely identify the RecordStore for consistent access from session to session.

Writing the Code

As shown previously, RecordStore usage is heavy on exception handling. For our purposes, it's good enough to use a catchall approach (pardon the Java pun) and just catch most exceptions in general.

Let's start off with the method to clear the RecordStore. It returns a Boolean that's true if the RecordStore was successfully removed (or simply didn't exist), and false if a different exception is encountered.

protected boolean removeImageStore()
{
    try
    {
    if (imageDB != null
    {
        imageDB.closeRecordStore();
    }
    RecordStore.deleteRecordStore(IMAGE_STORE);
    }
    catch (RecordStoreException rse)
    {
        // The RecordStore didn't exist to delete
        // in the first place. Shouldn't be a
        // problem.
        return true
    }
    catch (Exception e)
    {
        // Something happened we can't handle
        return false;
    }
    // Removed successfully
    return true;
}

Now let's build the first method, which grabs image data from the RecordStore.

First, we'll check whether the RecordStore of images even exists, and if not, just return null so that the MIDlet will know to get the images over the Internet. Then, we'll verify that the RecordStore is properly built (that is, simply has some records).

public byte[][] getImageDataFromStore()
{
    imageDB = null;
    try {
        imageDB = RecordStore.openRecordStore(IMAGE_STORE, false);

        // Get the number of records present
        int numRecords = imageDB.getNumRecords();
        // Make sure the RecordStore actually *has* some records
        if (numRecords == 0)
        {
            // The RecordStore seems to be whacked, so delete it
            // and abort
            removeImageStore();
            return null;
        }

Now let's set up the temporary variables needed to unpack the Images. If there's an issue, just return null and move on.

        byte[][] allRecordData = new byte[numRecords][];
        byte[] data = null;

        // Note that we're counting the records from 1,
        // not from 0.
        for (int i = 1; i <= numRecords; i++)
        {
            data = null;
            data = imageDB.getRecord(i);
            // Note that the array index is one less than the
            // recordId.
            allRecordData[i-1] = data;
        }

        // All done, close the RecordStore
        imageDB.closeRecordStore();

        // Success!
        return allRecordData;
    }
    catch (Exception e)
    {
        // There was a problem somewhere above.
        // Make sure the RecordStore is removed.
        removeImageStore();

        // And abort
        return null;
    }
}

As you can see, it's all pretty straightforward. Here's the sister method:

public void storeImageData(byte[][] imageData)
{

First, let's remove the old RecordStore and start a new one by calling removeImageRecord(). Remember the Boolean it returns? If it returns false, we abort because it might mean an issue with the RecordStore facility in general, which we can't handle.

Removing the RecordStore before writing to it isn't strictly necessary, but it will let us handle the same number of images from one session to another cleanly, without the need to monitor the record numbers and ids too closely.

If (!removeImageRecord())
{
    return;
}

Next, we build the RecordStore anew.

imageDB = null;

try {
    // This creates a new RecordStore
    imageDB = RecordStore.openRecordStore(IMAGE_STORE, true);

Now, let's make absolutely sure there's enough storage space for our byte arrays. This isn't really needed, as adding the actual data will result in an exception if there's not enough room, but it will let us stop the process before actually writing anything to the store.

        int totalSize = 0;
        for (int i = 0; i < imageData.length; i++)
        {
            totalSize += imageData[i].length;
        }

        if (totalSize > imageDB.getSizeAvailable())
        {
            // bad news, just abort and return
            removeRecordStore();
            return;
        }
        // If we made it to here, it looks good and we'll likely succeed.
        for (int i = 0; i < imageData.length; i++)
        {
            imageDB.addRecord(imageData[i], 0, imageData[i].length);
        }

        imageDB.closeRecordStore();
    }
    catch (Exception e)
    {
        // Clean up and abort
       removeImageStore();
    }
}

As you should expect, writing the records is a little more involved than simply reading them, because there's more chance that things could go wrong.

The MIDlet Changes

In the game MIDlet's code, we'll need to handle things as follows:

weaponImageSource = getImageDataFromStore();
if (weaponImageSource == null)
{
    // We couldn't build the data from
    // the RecordStore so we need to
    // get them via the Internet.

    weaponImageSource = getImageDataFromServer();

    if (weaponImageSource == null)
    {
        // Now we're in trouble, we couldn't
        // build the images any which way
        // so we'll need to inform the user
        // and possibly abort the game...
    }
}

// We got the data, now build the images

weaponImages = new Image[weaponImageSource.length];
for (int i = 0; i < weaponImageSource.length; i++)
{
    weaponImages[i] = Image.createImage(weaponImageSource[i], 0, weaponImageSource[i].length);
}

Okay, the Images are built! Now, when the user is done playing and closes out the MIDlet, the weaponImageSource array should still be alive and kept up-to-date with any weapon image changes, so we simply call

storeImageData(weaponImageSource);

That's it! The game can now store and retrieve its weapon images from the RecordStore.

More RecordStore Joy

Storing a byte array used to create an Image is about as simple as it gets for images, but with the right combination of classes, we can actually use records and their byte array data more simply and with greater flexibility for other types of objects. We'll also get into RecordEnumerator handling here. Consider the following class in Listing 19.1.

Example 19.1. The CarStore Class

public class CarStore
{
    // These two static final ints will
    // be used to filter the records via
    // a RecordFilter implementation.
    public static final int CASH_RECORD = 1;
    public static final int WEAPON_RECORD = 2;

    // The player's current weapons
    private Weapon[] weapons;

    // The player's current cash
    private int cash;

    // Build a new CarStore and set the
    // members
    CarStore(int cash, Weapon[] weapons) {
        this.cash = cash;
        this.weapons = weapons;
    }

    // get the current cash
    public int getCash()
    {
        return cash;
    }

    // get the current weapons array
    public Weapon[] getWeapons()
    {
        return weapons;
    }

    // Writes the cash and weapon data to a new
    // RecordStore.
    public void writeToStore()
    {
        try
        {
            // Let's make sure we're dealing with a fresh new RecordStore
            // (because we're still relying a little on recordId ordering)
            // and remove it if it's still in device storage.
            RecordStore.deleteRecordStore("MyCoolCar");

        }
        catch (Exception e)
        {
            // Shouldn't be a showstopper, so continue
        }

        // Now we can build the RecordStore from scratch
        try
        {
            // Let's make sure we're dealing with a fresh new RecordStore
            // (since we're still relying a little on recordId ordering)
            // and remove it if it's still in device storage.
            RecordStore.deleteRecordStore("MyCoolCar");
            // Now we'll create a new one.
            RecordStore carDB = RecordStore.openRecordStore("MyCoolCar", true);

            // Build some reusable data handling objects.
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            DataOutputStream dout = new DataOutputStream(bout);
            byte[] data = null;

            // Build some simple weapon description Strings
            // just to show the breadth of functionality we
            // have here.  This array is keyed to Weapon.FLAME
            // and Weapon.OIL, which are 0 and 1, respectively.

            String[] weaponName = {  "Flamer", "Oil" } ;

            // First, we're going to write the player's
            // current cash to the car's RecordStore
            // We'll id the record as a cash record...
            dout.writeByte(CASH_RECORD);

            // and then write the cash amount
            dout.writeInt(cash);

            // and now bundle the Stream into a byte
            // array and write a new record.  It's
            // the very first record in the RecordStore,
            // so we know it'll have the recordId of 1.
            data = bout.toByteArray();

            // Add the array to the RecordStore
            carDB.addRecord(data, 0, data.length );

            // and reset the Streams to
            // properly handle the Weapons.
            bout.reset();

            // Iterate through the car's Weapons array
            for (int i = 0; i < weapons.length; i++)
            {
                // Write the weapon record tag..
                dout.writeByte(WEAPON_RECORD);

                // Write the weapon name to the stream
                dout.writeUTF(weaponName[weapons[i].getWeaponType()]);

                // Write the weapon type...
                dout.writeInt(weapons[i].getWeaponType());

                // Write the weapon's ammo
                dout.writeInt(weapons[i].getWeaponAmmo());

                // and finally, the Weapon's time value
                dout.writeInt(weapons[i].getWeaponTime());

                dout.flush();

                // Pack the Byte Stream (aka the Data Stream)
                // into the data byte array...
                data = bout.toByteArray();

                // Add the array to the RecordStore
                carDB.addRecord(data, 0, data.length );

                // and reset the Streams to
                // properly handle the next Weapon.
                bout.reset();
            }	
            carDB.closeRecordStore();
            dout.close();
            bout.close();

        }
        catch (Exception e)
        {
            // handle exceptions here...
        }
}

Okay, that puts the cash and the weapons data into a RecordStore. To get it out, we're going to do things differently and use a RecordEnumerator with a custom implementation of the RecordFilter interface. If you noticed, the CASH_RECORD and WEAPON_RECORD values were written as bytes to their respective records before any of the other data. We'll use that byte to filter the records. Setting aside the CarStore class for a moment, here's our CarItemFilter class, shown in Listing 19.2.

Example 19.2. The CarItemFilter Class

class CarItemFilter implements RecordFilter
{
    // This will contain the car item id
    // we'll be looking for in the records.
    private byte filterValue;

    // Builds a new CarItemFilter for the
    // specific car record id we want to
    // find
    public CarItemFilter(int filterInt)
    {
        filterValue = (byte)filterInt;
    }

    // This is the RecordFilter method used
    // by the RecordEnumeration to determine
    // if the record is a "match" and needs
    // to be included in its enumeration.
    public boolean matches(byte[] candidate)
    {
        if (candidate == null || candidate.length == 0)
        {
            return false;
        }

        // Since, in the CarStore.writeToStore() method,
        // we wrote the item type byte to the records
        // before anything else, the byte at index 0
        // should be our id to filter.
        return (candidate[0] == filterValue);
    }
}

Alright! Now we're ready to get our data back out of the RecordStore. Here's the CarStore method that will do it for us:

public void readStore()
{
    try
    {
        // Open the RecordStore built above...
        RecordStore carDB = RecordStore.openRecordStore ("MyCoolCar", false);

        // Build some reusable data handling objects.
        ByteArrayInputStream bin = null;
        DataInputStream din = null;
        byte[] data = null;

        // First, we'll get the cash record out and set
        // the cash value.  Remember that this record
        // was added before any other so we know it's
        // at recordId == 1.
        data = carDB.getRecord(1);

        // Set up the streams to read back the
        // stored variables
        bin = new ByteArrayInputStream(data);
        din = new DataInputStream(bin);

        // discard the CASH_RECORD byte
        din.readByte();

        // and now grab the actual cash value stored.
        cash = din.readInt();

        // clean up for the weapons handling
        din.close();
        bin.close();

        // Prepare the Weapons array for the new data
        // (We build it to (getNumRecords()-1) to account
        // for the cash record.)
        weapons = new Weapon[carDB.getNumRecords()-1];

        // The following sets up a RecordEnumeration
        // object with our CarItemFilter RecordFilter
        // and a null RecordComparator which means
        // it will simply iterate the weapon records
        // in no particular order.  We're assuming, here,
        // that the Weapon records found won't need to be
        // in a certain sequence but can be rebuilt
        // willy-nilly.  You can always build your
        // own RecordComparator to put the enumeration into
        // some specific order.
        CarItemFilter filter = new CarItemFilter(WEAPON_RECORD);
        RecordEnumeration re = carDB.enumerateRecords(filter, null, false);

        // Prepare the necessary temp variables and
        int count = 0;
        String weaponName = "";
        int type = 0;
        int time = 0;
        int ammo = 0;
        while (re.hasNextElement())
        {
            // This next call returns the record's byte array and
            // advances the RecordEnumerator's pointer to the record
            // beyond the one returned.
            data = re.nextRecord ();

            // Set up the streams...
            bin = new ByteArrayInputStream(data);
            din = new DataInputStream(bin);

            // Read and discard the weapon record id
            din.readByte();

            // Read back the weapon name String
            weaponName = din.readUTF();
            System.out.println("Got the Weapon name: "+weaponName);

            // Read the weapon type...
            type = din.readInt();

            // Read the weapon's ammo
            ammo = din.readInt();

            // and finally, the Weapon's time value
            time = din.readInt();

            // Now rebuild the weapon into the
            // weapons array.
            weapons[count++] = new Weapon(type, time, ammo);
            // Clean up
            din.close();
            bin.close();
        }
    }
    catch (Exception e)
    {
        // handle exceptions here...
    }
  }
}

The RecordEnumeration's behavior can be further refined by building an implementation of RecordComparator. For example, we could easily implement one and make a RecordEnumerator to return weapon records ordered according to the ammo value.

NOTE

Using filters and comparators slows down the process. Using null for both ensures the fastest record retrieval possible with a RecordEnumerator, but will return all the records in the RecordStore in no specific guaranteed order.

There's one last interesting bit we haven't touched on yet with RecordStores. You can add a RecordListener to a RecordStore via its addRecordListener() method. The class implementing RecordListener will then be notified any time any record is added, changed, or deleted from the RecordStore.

Summary

In this chapter we looked at when (and when not) to use J2ME's RecordStore class, along with its limitations and practical work-arounds. Now you know how to directly store and retrieve data from a RecordStore, get a helping hand from the java.io package, and use RecordFilters and RecordEnumerations to store and retrieve data flexibly and consistently.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.165.70