Persistence (RMS)When developing a game, you'll want to save (or "persist") data that you can retrieve later, after the gameor even the phoneis shut off. Like most things in the J2ME world, the functionality is there, it's just in the "It's life, Jim, but not as we know it" category. Persisting data on a MID is done via the RMS (Record Management System), which you'll find in the javax.microedition.rms package (Table 5.7 contains a list of all the classes within this package). The RMS stores data as records, which are then referenced using a unique record key. Groups of records are stored in the rather inventively named record store.
Record StoreI had real trouble with that heading; so many lame puns, so little time....Anyway, a record store is exactly what the term impliesa storage mechanism for records. You can see the complete API available in Table 5.8.
As you can see in Figure 5.4, a record store exists in MIDlet suite scope. This means that any MIDlet in the same suite can access that suite's record store. MIDlets from an evil parallel universe (such as another suite) aren't even aware of the existence of your suite's record stores. Figure 5.4. A MIDlet only has access to any record stores created in the same MIDlet suite.
RecordA record is just an array of bytes in which you write data in any format you like (unlike a database table's predetermined table format). You can use DataInputStream, DataOutputStream, and of course ByteArrayInputStream and ByteArrayOutputStream to write data to a record. As you can see in Figure 5.5, records are stored in the record store in a table-like format. An integer primary key uniquely identifies a given record and its associated byte array. The RMS assigns record IDs for you; thus, the first record you write will have the ID of 1, and the record IDs will increase by one each time you write another record. Figure 5.5. A record store contains records, each with a unique integer key associated with a generic array of bytes.
Figure 5.5 also shows some simple uses for a record store. In this example, the player's name (the string "John") is stored in record 1. Record 2 contains the highest score, and record 3 is a cached image you previously downloaded over the network. NOTE
Note Practically, you likely would store all the details on a player, such as his name, score, and highest level, as a single record with one record per new player. You would then store all these records in a dedicated Player Data record store. You might also have noticed that there is no javax.microedition.rms.Record class. That's because the records are just arrays of bytes; all the functionality you need is in the RecordStore class. Take a look at an example now. In the following code, you'll create a record store, write out some string values, and then read them back again. Doesn't sound too hard, right? NOTE
Tip You can see the complete source code for this in the SimpleRMS.java on the CD (Chapter 5 source code). import java.io.*; import javax.microedition.midlet.*; import javax.microedition.rms.*; /** * An example of how to use the MIDP 1.0 Record Management System (RMS). * @author Martin J. Wells */ public class SimpleRMS extends javax.microedition.midlet.MIDlet { private RecordStore rs; private static final String STORE_NAME = "My Record Store"; /** * Constructor for the demonstration MIDlet does all the work for the tests. * It firstly opens (or creates if required) a record store and then inserts * some records containing data. It then reads those records back and * displays the results on the console. * @throws Exception */ public SimpleRMS() throws Exception { // Open (and optionally create a record store for our data rs = RecordStore.openRecordStore(STORE_NAME, true); // Create some records in the store String[] words = {"they", "mostly", "come", "at", "night"}; for (int i=0; i < words.length; i++) { // Create a byte stream we can write to ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); // To make life easier use a DataOutputStream to write the bytes // to the byteStream (ie. we get the writeXXX methods) DataOutputStream dataOutputStream = new DataOutputStream(byteOutputStream); dataOutputStream.writeUTF(words[i]); // ... add other dataOutputStream.writeXXX statements if you like dataOutputStream.flush(); // add the record byte[] recordOut = byteOutputStream.toByteArray(); int newRecordId = rs.addRecord(recordOut, 0, recordOut.length); System.out.println("Adding new record: " + newRecordId + " Value: " + recordOut.toString()); dataOutputStream.close(); byteOutputStream.close(); } // retrieve the state of the store now that it's been populated System.out.println("Record store now has " + rs.getNumRecords() + " record(s) using " + rs.getSize() + " byte(s) " + "[" + rs.getSizeAvailable() + " bytes free]"); // retrieve the records for (int i=1; i <= rs.getNumRecords(); i++) { int recordSize = rs.getRecordSize(i); if (recordSize > 0) { // construct a byte and wrapping data stream to read back the // java types from the binary format ByteArrayInputStream byteInputStream = new ByteArrayInputStream(rs.getRecord(i)); DataInputStream dataInputStream = new DataInputStream(byteInputStream); String value = dataInputStream.readUTF(); // ... add other dataOutputStream.readXXX statements here matching the // order they were written above System.out.println("Retrieved record: " + i + " Value: " + value); dataInputStream.close(); byteInputStream.close(); } } } /** * Called by the Application Manager when the MIDlet is starting or resuming * after being paused. In this case we just exit as soon as we start. * @throws MIDletStateChangeException */ protected void startApp() throws MIDletStateChangeException { destroyApp(false); notifyDestroyed(); } /** * Called by the MID's Application Manager to pause the MIDlet. A good * example of this is when the user receives an incoming phone call whilst * playing your game. When they're done the Application Manager will call * startApp to resume. For this example we don't need to do anything. */ protected void pauseApp() { } /** * Called by the MID's Application Manager when the MIDlet is about to * be destroyed (removed from memory). You should take this as an opportunity * to clear up any resources and save the game. For this example we don't * need to do anything. * @param unconditional if false you have the option of throwing a * MIDletStateChangeException to abort the destruction process. * @throws MIDletStateChangeException */ protected void destroyApp(boolean unconditional) throws MIDletStateChangeException { } } Seems like a lot of work to write a few strings, doesn't it? Fortunately it's not quite as complicated as it looks. The first thing you did was open the record store using the call rs = RecordStore.openRecordStore(STORE_NAME, true);. The Boolean argument on the call to openRecordStore indicates that you want to create a new record store if the one you named doesn't already exist. The next section creates and then writes a series of records to the record store. Because you have to write bytes to the record, I recommend using the combination of a ByteArrayOutputStream and DataOutputStream. The following code creates our two streamsfirst the ByteArrayOutputStream, and then a DataOutputStream, which has a target of the ByteArrayOutputStream. As you can see in Figure 5.6, this means that any data you write to using the very convenient writeXXX methods of this class will in turn be written in byte array format through the associated ByteArrayOutputStream. Figure 5.6. DataOutputStreams make byte array formatting easier.
The code to create this "stream train" is ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); DataOutputStream dataOutputStream = new DataOutputStream(byteOutputStream); You can then use the DataOutputStream convenience methods to write the data before flushing the stream (thus ensuring that everything is committed to the down streams). dataOutputStream.writeUTF(words[i]); dataOutputStream.flush(); Adding the record is simply a matter of grabbing the byte array from the ByteArrayOutputStream and sending it off to the RMS. byte[] recordOut = byteOutputStream.toByteArray(); int newRecordId = rs.addRecord(recordOut, 0, recordOut.length); Simple, huh? Here's the output: Adding new record: 1 Value: [B@ea0ef881 Adding new record: 2 Value: [B@84aee8b Adding new record: 3 Value: [B@c5c7331 Adding new record: 4 Value: [B@e938beb1 Adding new record: 5 Value: [B@11eaa96 Record store now has 5 record(s) using 208 byte(s) [979722 bytes free] Retrieved record: 1 Value: they Retrieved record: 2 Value: mostly Retrieved record: 3 Value: come Retrieved record: 4 Value: at Retrieved record: 5 Value: night You can see how easily you can write other data using the various write methods in your DataOutputStream. Just be sure you always read things back in the correct order. LockingOne nice aspect of the RMS implementation is that it takes care of locking for you. The record store implementation guarantees synchronized access, so there is no chance of accidentally accessing storage while another part of your MIDlet, or even another MIDlet in your suite, is hitting it at the same time. Since this type of protection is inherent, you don't need to go to the trouble of coding it yourself. EnumeratingWhen you retrieved the records in the previous examples, you used a simple method of reading back the data (an indexing for loop). However, you'll encounter cases in which you want to retrieve only a subset of records, possibly in a particular order. RMS supports the ordering of records using the javax.microedition.rms.RecordEnumerator class. Table 5.9 lists all the methods in this class.
You can access an enumerator instance using the record store. For example: RecordEnumeration enum = rs.enumerateRecords(null, null, false); while (enum.hasNextElement()) { byte[] record = enum.nextRecord() ; // do something with the record ByteArrayInputStream byteInputStream = new ByteArrayInputStream(record); DataInputStream dataInputStream = new DataInputStream(byteInputStream); String value = dataInputStream.readUTF(); // ... add other dataOutputStream.readXXX statements here matching // the order they were written above System.out.println(">"+value); } enum.destroy(); You can use the enumerator to go both forward and backward through the results. If you want to go backward, just use the previousRecord method. ComparingThe previous example retrieves records in ID order, but you can change this order using the ...wait for it . . . RecordComparator (the API is shown in Table 5.10).
You can use the javax.microedition.rms.RecordComparator interface as the basis for a class that will bring order to your enumerated chaos. All you need to do is create a class that compares the two records and returns an integer value representing whether one record is equivalent to, precedes, or follows another record. NOTE
Note The javax.microedition.rms.RecordComparator interface includes the following convenience definitions for the compare method return values:
Here's an example of a record comparator to sort the string values of the previous examples: class StringComparator implements RecordComparator { public int compare(byte[] bytes, byte[] bytes1) { String value = getStringValue(bytes); String value1 = getStringValue(bytes1); if (value.compareTo(value1) < 0) return PRECEDES; if (value.compareTo(value1) > 0) return FOLLOWS; return EQUIVALENT; } private String getStringValue(byte[] record) { try { ByteArrayInputStream byteInputStream = new ByteArrayInputStream(record); DataInputStream dataInputStream = new DataInputStream(byteInputStream); return dataInputStream.readUTF(); } catch(Exception e) { System.out.println(e.toString()); return ""; } } } You can then use this comparator in any call to create an enumeration. For example: RecordEnumeration enum = rs.enumerateRecords(null, new StringComparator(), false); If you were to then enumerate through the records from your previous examples, they would be displayed in a sorted order according to the string value stored in each record. The output from running this against your previous record store's data follows. (Sounds uncannily like Yoda, doesn't it?) Retrieved record: 4 Value: at Retrieved record: 3 Value: come Retrieved record: 2 Value: mostly Retrieved record: 5 Value: night Retrieved record: 1 Value: they If you have records containing more complicated data, you need to have your comparator make a logical appraisal of the contents of each record, and then return an appropriate integer to represent the desired order. You can see a complete working example of this in the SortedRMS.java file on the CD (in the Chapter 5 source code). FilteringSorting enumerations is great! I can't think of anything else I'd like to be doing on a Saturday than sorting enumerators, but sometimes you'll want to limit or filter the records you get back from an enumerator. Enter the javax.microedition.rms.RecordFilter class.....You can see the one and only method in this class defined in Table 5.11.
Filtering is just as easy as comparing. (I'm assuming you found that easy.) You just create a class that implements the javax.microedition.rms.RecordFilter and then implement the required methods. Here's an example: class StringFilter implements RecordFilter { private String mustContainString; public StringFilter(String mustContain) { // save the match string mustContainString = mustContain; } public boolean matches(byte[] bytes) { // check if our string is in the record if (getStringValue(bytes).indexOf(mustContainString) == -1) return false; return true; } private String getStringValue(byte[] record) { try { ByteArrayInputStream byteInputStream = new ByteArrayInputStream(record); DataInputStream dataInputStream = new DataInputStream(byteInputStream); return dataInputStream.readUTF(); } catch (Exception e) { System.out.println(e.toString()); return ""; } } } To use your new filter, just instantiate it in the call to construct the enumerator, like you did with the comparator. RecordEnumeration enum = rs.enumerateRecords(new StringFilter("o"), new StringComparator(), false); Note that I'm using the comparator and the filter together. It's all happening now! The output from this is now limited to records with a string value containing an "o" (the result of the indexOf("o") call returning something other than 1). Thus you would only see Retrieved record: 3 Value: come Retrieved record: 2 Value: mostly Listening InThe RMS also has a convenient interface you can use to create a listener for any RMS events that occur. Using this, you can make your game react automatically to changes to a record store. This is especially important if such a change has come from another MIDlet in your suite because you won't be aware of the change. To create a listener, you need to make a class that implements the javax.microedition. rms.RecordListener interface (Table 5.12 lists the methods).
For example, the following is an inner class that simply outputs a message whenever an event occurs: class Listener implements javax.microedition.rms.RecordListener { public void recordAdded(RecordStore recordStore, int i) { try { System.out.println("Record " + i + " added to " + recordStore.getName()); } catch(Exception e) { System.out.println(e); } } public void recordChanged(RecordStore recordStore, int i) { try { System.out.println("Record " + i + " changed in " + recordStore.getName()); } catch (Exception e) { System.out.println(e); } } public void recordDeleted(RecordStore recordStore, int i) { try { System.out.println("Record " + i + " deleted from " + recordStore.getName()); } catch (Exception e) { System.out.println(e); } } } NOTE
Note The source file ListenerRMS.java on the CD has a complete example of a working RMS Listener. To activate this listener, use the RecordStore.addListener method on a record store you have previously created. This can be used anywhere where you have a record store and want to be notified when it is accessed, for example: RecordStore rs = RecordStore.openRecordStore("Example", true); rs.addRecordListener(new Listener()); ExceptionsI want to make a quick note about exceptions before you leave the world of RMS. In the preceding examples, I've ignored or hacked up pathetic handlers for run-time exceptions that might be thrown. Table 5.13 lists all of these exceptions.
In most cases RMS exceptions occur due to abnormal conditions in which you need to either code around the problem (in the case of the RecordStoreNotFoundException, RecordStoreNotOpenException, and InvalidRecordIDException) or just live with it (in the case of RecordStoreException). The one possible exception to this is RecordStoreFullException, which you might resolve by having your MIDlet clear some space and try again. Either way, when developing your game, consider the implications of these exceptions and do your best to handle them gracefully, even if all you can do is inform the player that his game save failed. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||