Manipulating Record Stores and Records



Manipulating Record Stores and Records

Record stores have two basic types of operations: those that deal with manipulating the record store as a whole and those that deal with manipulating the records within the record store. A summary of different record store and record manipulation operations is provided in the following sections.

14.2.1 Manipulating a Record Store

The programmer accesses record stores by name. These names are case-sensitive and may contain between 1 and 32 Unicode characters. The name space for record stores is a flat, non-hierarchical space.[1]

[1] Note: The name space of record stores is separate from that of resources (resources are accessed by calling the method java.lang.Class.getResourceAsStream.) Thus, the name of a record store can be lexically equivalent to that of a resource, but the two are separate entities.

Within a MIDlet suite, record store names are unique. In other words, MIDlets within a MIDlet suite are not allowed to create more than one record store with the same name. On the other hand, no such restriction applies to different MIDlet suites: a MIDlet within one MIDlet suite is allowed to have a record store with the same name as a MIDlet in another MIDlet suite. In that case, the record stores are distinct and separate. A MIDlet can obtain a list of all the record stores owned by the containing MIDlet suite by calling the class static method listRecordStores of class RecordStore.

In order to access a record store, the record store must be opened first. A MIDlet can open one of its record stores with the method openRecordStore that takes two parameters: a String containing the name of the record store, and a boolean indicating whether the record store should be created if it does not exist yet. The method openRecordStore returns an object of type RecordStore, which provides access to the following methods that return information about the associated record store:

  • getName: returns a String that represents the name with which this record was opened or created.

  • getNumRecords: returns an int that represents the number of records currently in the record store.

  • getSize: returns an int that represents the current number of bytes contained in the record store.

  • getSizeAvailable: returns an int that represents the number of remaining bytes in the device that the record store could potentially occupy. Note that the number returned by this method is not a guarantee that the record store can actually grow to this size, since another MIDlet may allocate space for another record store.

  • getNextRecordID: returns an int that represents the recordId for the next record added to the record store.

  • getVersion: returns an int that represents the version of the record store. Each time a record is added, deleted, or modified in the record store, the version number of the record store is incremented by 1.

  • getLastModified: returns a long that represents the last modification time of the record store. The time returned is in the same format as used by method System.currentTimeMillis. The modification time of a record store is changed each time its version changes (see the description above.)

The two final operations on a record store are closing and deleting it. A record store is closed by invoking the method closeRecordStore. Note that a record store is not closed until all outstanding opens have been matched with a corresponding close operation. In other words, as long as there are open references to a record store, it will not be closed. When a record store is finally closed, all record listeners are removed (see Section 14.2.5, "Filtering, Comparing, Listening, and Enumerating Records"), and any attempts to perform any operations on the record store object cause a RecordStoreNotOpenException to be thrown.

A record store is deleted by passing its name as a String to the static method deleteRecordStore of class RecordStore. A record store must be closed before it can be deleted.

2 Shared Record Stores

graphics/new_icon.gif

New APIs in MIDP 2.0 allow for the explicit sharing of record stores if the MIDlet creating the record store chooses to give such permission. At the time of record store creation, a MIDlet can choose whether the record store should be available only to MIDlets within its MIDlet suite or available to all MIDlets installed on the device. If a record store is shared, the MIDlet can also choose whether MIDlets in other MIDlet suites should have read-only access or be allowed to write to the record store. There are no fine-grained access controls available for granting access to only a subset of the MIDlets on a device.

To create a shared record store, the MIDlet calls the openRecordStore method, which takes four parameters:

  • a String containing the name of the record store,

  • a boolean indicating whether the records store should be created (if it does not yet exist),

  • an integer indicating the access mode requested (shared or private), and

  • a boolean indicating whether MIDlets from other MIDlet suites are granted write access to the record store, or only read-only access.

Sharing is accomplished through the ability to name a record store created by another MIDlet suite. That is, record stores are identified using the unique name of the MIDlet suite plus the name of the record store. MIDlet suites are identified by the MIDlet-Vendor and MIDlet-Name attributes from the manifest and/or application descriptor (see Section 19.1.3, "MIDlet Attributes").

To open a shared record store from another MIDlet suite, the MIDlet calls the openRecordStore method that takes three String parameters: the name of the record store, and the vendor name and MIDlet suite name of the MIDlet suite that created the shared record store.

As described in Section 19.3.2, "MIDlet Suite Removal," when a MIDlet suite is deleted all of its record stores are also removed. This includes any shared record stores that it owns.

14.2.3 Manipulating Records in a Record Store

Within a record store, individual records can be added, retrieved, modified, and deleted. The most common methods for manipulating records are summarized below:

  • addRecord: takes a byte array, an offset, and a length as parameters. A recordId is returned for a record created from the byte array subset starting at the given offset through the given length. Note that this is a blocking operation and the method will not return until the record has been written to the underlying persistent storage.

  • deleteRecord: takes a recordId as a parameter and removes the record represented by that recordId from the record store.

  • getRecordSize: takes a recordId as a parameter and returns the size of the associated record in bytes.

  • getRecord: has two forms: The first form takes a recordId as a parameter and returns a copy of the record. The second form takes a recordId, and a user-supplied byte array and offset. This form of the method returns a copy of the record into the byte array starting at the given offset.

  • setRecord: takes a recordId, a byte array, an offset, and a length as parameters. The record identified by the recordId is overwritten from the beginning with data from a subset of the byte array subset identified by the offset and length.

14.2.4 Converting Record Data to and from Byte Arrays

Before data can be saved into a record store, it must be converted into an array of bytes. Using CLDC classes such as DataInputStream, DataOutputStream, ByteArrayInputStream, or ByteArrayOutputStream, developers can pack and unpack different data types into and out of byte arrays. As an example, consider a game MIDlet that needs to record scores and names of players. The code fragment below illustrates how such a record (byte array) might be created:

ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream outputStream = new DataOutputStream(baos);
// Add the score (an int) then the name of the player (a UTF)
// to the DataOutputStream.
outputStream.writeInt(score);
outputStream.writeUTF(name);
// Now, extract the byte array (record)
byte[] theRecord = baos.toByteArray();

Once the record has been created, it can be inserted into a record store using the addRecord operation introduced in Section 14.2.3, "Manipulating Records in a Record Store."

14.2.5 Filtering, Comparing, Listening, and Enumerating Records

The records of a record store may be processed in various ways. To provide programmer-defined ways to filter, search, and sort a record store, RMS provides the four Java interfaces discussed below.

The RecordFilter interface

The RecordFilter interface allows the programmer to define filters for searching records. This interface requires the programmer to supply a method called matches that accepts one record as a parameter. This method returns a boolean indicating whether or not the given record matches the user-defined search criteria. For instance, consider the following example in which a record store contains different types of records, with the type being indicated by the first byte. In this example, Integer records are indicated by the first byte being 'I'. The following filter returns true if the candidate record is an Integer record:

public class IntegerFilter implements RecordFilter {
    public boolean matches(byte[] candidate)
                   throws IllegalArgumentException {
        return candidate[0] == 'I';
    }
}
The RecordComparator interface

The RecordComparator interface allows the programmer to define comparison operations for records. This interface defines the method compare that takes two records as parameters. This method returns one of the following class-static values from the RecordComparator class:

  • EQUIVALENT: returned if the two records are considered equivalent for this sort order.

  • FOLLOWS: returned if the first record follows the second for this sort order.

  • PRECEDES: returned if the first record precedes the second for this sort order.

To carry forth the previous example, let's assume that the Integer records discussed above have the format of a record type indicator (in this case: 'I') followed by a four-byte Integer. Furthermore, let's assume that we want to sort the records in ascending order according to value of the four-byte Integer field. A record comparator to accomplish this could be written as follows (with error checking omitted):

class IntegerCompare implements RecordComparator {
    public int compare(byte[] b1, byte[] b2) {
        DataInputStream is1 = new DataInputStream(
                                  new ByteArrayInputStream(b1));
        DataInputStream is2 = new DataInputStream(
                                  new ByteArrayInputStream(b2));
        /**
         * Skip record type (1 byte), then read ints
         */
        is1.skip(1);
        is2.skip(1);
        int i1 = is1.readInt();
        int i2 = is2.readInt();
        if (i1 > i2) return RecordComparator.FOLLOWS;
        if (i1 < i2) return RecordComparator.PRECEDES;
        return RecordComparator.EQUIVALENT;
    }
}
The RecordListener interface

In order to monitor the addition and modifications of records to a record store, RMS provides the RecordListener interface. This interface defines three methods that a class must implement. Each method takes two parameters: a RecordStore and a recordID:

  • recordAdded: called when a record is added to the record store.

  • recordChanged: called when a record is modified in the record store.

  • recordDeleted: called when a record is deleted in the record store.

The following code fragment shows a simple record listener that prints out a line indicating what type of operation is being done to a record store:

/**
 * Add the listener to the record store
 */
recordStore.addRecordListener((RecordListener) new myListener());
...
public class myListener implements RecordListener {
    final private static int ADDED = 1;
    final private static int CHANGED = 2;
    final private static int DELETED = 3;

    private void handleCall(int type, RecordStore rs, int rid) {
        System.out.print("Record store " + rs.getName() +
                         " and record id " + rid +
                         " was ");
        switch (type) {
        case myListener.ADDED:
            System.out.println("added");
            break;
        case myListener.CHANGED:
            System.out.println("changed");
            break;
        case myListener.DELETED:
            System.out.println("deleted");
            break;
        }
    }

    public void recordAdded(RecordStore rs, int rid) {
        handleCall(myListener.ADDED, rs, rid);
    }
    public void recordChanged(RecordStore rs, int rid) {
        handleCall(myListener.CHANGED, rs, rid);
    }
    public void recordDeleted(RecordStore rs, int rid) {
        handleCall(myListener.DELETED, rs, rid);
    }
}
The RecordEnumerator interface

The final interface provided by RMS is the RecordEnumeration interface. A class implementing this interface provides a mechanism to enumerate over all the records in the record store. Although a developer can implement this interface, the most common usage of record enumeration is simply using the enumerateRecords provided by the RecordStore class.

The enumerateRecords method returns an instance of a class that implements the RecordEnumeration interface. It takes three parameters: a RecordComparator, a RecordFilter, and a boolean that indicates that the returned enumerator should register itself as a listener in order to keep it updated with ongoing activity on the record store. The most efficient way to get each record in a record store is to supply neither a RecordComparator nor a RecordFilter:

simpleRE = recordStore.enumerateRecords(null, null, false);
while (re.hasNextElement()) {
    byte b[] = re.nextRecord();
    ...
}

This example would return, in an undefined order, every record in the record store while ignoring changes made to the record store by other MIDlets or threads.

A more complex example is enumerating through a subset of records in the record store and sorting them in a user-defined manner. Using our previous examples of the RecordComparator and the RecordFilter, we can construct a record enumerator that only returns the records containing Integer numbers in numerically ascending order with the following code fragment:

IntegerFilter iFilt = new IntegerFilter();
IntCompare iCompare = new IntegerCompare();
intRE = recordStore.enumerateRecords((RecordFilter)iFilt,
                                     (RecordComparator)iCompare,
                                     false);
while (IntRE.hasNextElement()) {
    byte b[] = re.nextRecord();
    ...
}

Suppose that our record store had two types of records: Integer and String, with the String records starting with an 'S' and the Integer records beginning with an 'I'. If our record store contained the following records:

Record 1: 'I', 100
Record 2: 'S', "Joy"
Record 3: 'I', 50
Record 4: 'S', "Zachary"
Record 5: 'S', "Abby"
Record 6: 'I', 25

then the enumeration returned by intRE would be Record 6, Record 3, then Record 1.