ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Building Wireless Web Clients, Part 2
Pages: 1, 2, 3

Getting the List of Books in the Bookstore

In the PersistentRankingMIDlet, we need to get the set of BookInfo objects for all of the books that the user has stored in the RecordStore when the MIDlet starts up. If there are no records available (as will be the case when it is executed for the first time following installation or if all previously stored records have been deleted), we need to display the screen that allows the user to enter an ISBN. Otherwise, the list of books should be displayed so that the user can select one to view.



The only way to retrieve a record from the RecordStore is to call the getRecord() method, passing the required recordId. The problem is, there is no way to know which recordId values correspond to valid records. One way to tackle this problem would be to use code like this:

for (int i = 1, count = store.getNextRecordID(); i < count; i++) {
    try {
    byte[] bytes = store.getrecord(i);

    
    // do something with the record
    } catch (invalidrecordidexception ex) {
    // no record for this record id
    }
}

The trouble with this code is that it is going to be very slow after the RecordStore has been in use for some time. As long as records have not been deleted, this code will behave properly. Consider, however, the extreme case in which 100 records have been added to the RecordStore over time, but only the first and the most recent remain. This means that record IDs 1 and 100 are currently valid, but 2 through 99 are not. In order to discover this, the above loop iterates over all 100 possible IDs, successfully reading the first, but then encountering 98 successive exceptions for invalid record IDs, before reading record 100. Not only is this a waste of 98 iterations of the loop, it is also going to be slow, because it is very time-consuming to construct and throw an exception. Incidentally, it is possible to create this situation with as little as two records in the RecordStore at any given time because record IDs are never reused. Here is how that might happen:

  1. Enter the first record, which gets record ID 1.
  2. Enter the second record, which gets record ID 2.
  3. Delete the second record, making record ID 2 invalid.
  4. Enter the third record, which gets record ID 3.
  5. Delete the third record, making record ID 3 invalid.
  6. Enter the fourth record, which gets record ID 4.
  7. and so on...

Using a RecordEnumeration

Fortunately, there is a much more efficient way to tackle this problem. Instead of walking through all of the record IDs that have ever been assigned, you can get a RecordEnumeration that contains any subset of the content of a RecordStore. A RecordEnumeration is like a java.util.Enumeration, in that it allows you to iterate over a collection of objects. It is, however, more powerful than Enumeration because you can traverse the collection either forward or backward, and you can change direction at any time. For further details, refer to the API reference or Chapter 6 of J2ME in a Nutshell.

In order to get a RecordEnumeration, use the following RecordStore method:

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

RecordFilter and RecordComparator are interfaces that define methods that allow you to exclude records from the enumeration and determine the order in which the records are returned, respectively. If the filter argument is null, then all records are included, while a null comparator causes the order of the records to be undefined. The following call, therefore, returns a RecordEnumeration containing all of the records of the RecordStore in no particular order:

RecordEnumeration enum = recordStore.enumerateRecords(null, null, false);

The keepUpdated argument determines whether the enumeration is static or dynamic. If this argument is false, the enumeration represents a snapshot of the state of the RecordStore when enumerateRecords() is called. If keepUpdated is true, however, changes in the content of the RecordStore will be visible through the enumeration (unless the changes involve records that are excluded by the filter). It is more efficient to create an enumeration with keepUpdated set to false, because it can be expensive to keep the enumeration in step with the underlying RecordStore. An alternative way to react to changes is to register a RecordListener and handle them in your own code.

The RecordFilter Interface

The RecordFilter interface defines a single method:

public boolean matches(byte[] data)

This method should be implemented to return true if the record whose content is passed to it meets the filter criterion and false if it does not. Only those records for which true is returned will be included in the RecordEnumeration. The BookStore class contains an example of a RecordEnumeration used with a RecordFilter in the saveBookInfo method:

// Adds an entry to the store or modifies the existing
// entry if a matching ISBN exists.
    public void saveBookInfo(BookInfo bookInfo)
                                throws IOException, RecordStoreException {
    if (store != null) {
      <b>searchISBN = bookInfo.getIsbn();
      RecordEnumeration enum = store.enumerateRecords(
                    this, null, false);
      if (enum.numRecords() > 0) {</b>
        // A matching record exists. Set the id
        // of the BookInfo to match the existing record
        bookInfo.id = enum.nextRecordId();
        byte[] bytes = toByteArray(bookInfo);
        store.setRecord(bookInfo.id, bytes, 0, bytes.length);
      } else {
        // Create a new record
        bookInfo.id = store.getNextRecordID();
        byte[] bytes = toByteArray(bookInfo);
        store.addRecord(bytes, 0, bytes.length);
      }

      // Finally, destroy the RecordEnumeration
      enum.destroy();
    }
}

This method stores the content of a BookInfo record in the BookStore. It needs to create a new record by calling addRecord() if the book does not already have an entry, or update the existing entry using setRecord() if it does. In order to do this, it needs to search the RecordStore for a record with the same ISBN as the one in the BookInfo object. As before, it would be inefficient to use a loop over all of the possible record IDs to find a record with a given ISBN. Instead, this code uses the enumerateRecords() method with a filter that returns true only for a record with the matching ISBN, but with no comparator. The enumeration will consist of either one record or no records.

In this case, the BookStore class itself implements the RecordFilter interface, so the filter reference is passed as this and the ISBN to be searched for is saved in the searchISBN instance variable. Here is the implementation of the filter:

// RecordFilter implementation
public boolean matches(byte[] book) {
    if (searchISBN != null) {
      try {
        DataInputStream stream =
          new DataInputStream(new ByteArrayInputStream(book));

        // Match based on the ISBN.
        return searchISBN.equals(stream.readUTF());
      } catch (IOException ex) {
        System.err.println(ex);
      }
    }

    // Default is not to match
    return false;
}

This method receives the content of a record in the form of a byte array, then wraps a ByteArrayInputStream and a DataInputStream around it so that the fields within the record can be obtained. As shown in the implementation of the toByteArray() method above, the ISBN is the first field written to the record, so it can easily be retrieved by calling the readUTF() method of DataInputStream and comparing it to the ISBN being searched for.

The RecordComparator Interface

Like RecordFilter, RecordComparator defines only one method:

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

This method is passed the content of two records and is expected to compare them to determine their relative ordering. The return value should be RecordComparator.PRECEDES if rec1 comes before rec2, RecordComparator.FOLLOWS if rec1 comes after rec2, and RecordComparator.EQUIVALENT if they are equivalent (have the same ISBN). The BookStore class uses a comparator when retrieving the list of all books for which there is stored information, from which the list initially displayed to the user is constructed:

// Gets a sorted list of all of the books in
// the store.
public RecordEnumeration getBooks() throws RecordStoreException {
    if (store != null) {
        return store.enumerateRecords(null, this, false);
    }
    return null;
}

Here, a RecordEnumeration that contains all of the records in the RecordStore is created, sorted according to the comparator, which is implemented by the BookStore class itself:

  // RecordComparator implementation
  public int compare(byte[] book1, byte[] book2) {
    try {
      DataInputStream stream1 =
        new DataInputStream(new ByteArrayInputStream(book1));
      DataInputStream stream2 =
        new DataInputStream(new ByteArrayInputStream(book2));

      // Match based on the ISBN, but sort based on the title.
      String isbn1 = stream1.readUTF();
      String isbn2 = stream2.readUTF();
      if (isbn1.equals(isbn2)) {
        return RecordComparator.EQUIVALENT;
      }
      String title1 = stream1.readUTF();
      String title2 = stream2.readUTF();
      int result = title1.compareTo(title2);
      if (result == 0) {
        return RecordComparator.EQUIVALENT;
      }
      return result < 0 ? recordcomparator.precedes :
                recordcomparator.follows;
    } catch (ioexception ex) {
      return recordcomparator.equivalent;
    }
  }

This method is really much simpler than it looks. All it is doing is wrapping both records with streams that allow the primitives to be extracted, and then extracting both the ISBN and title fields from both. The records are considered to be equivalent if they have the same ISBN. Otherwise, the ordering is determined by an alphabetical comparison of the books' titles.

Deleting a Book's Entry from the Bookstore

The last operation we need to be able to perform is to remove the entry for a book when the user selects the delete option from the menu shown in Figure 1. This is very simple to implement, using the deleteRecord() of the RecordStore class. The only attribute we need to perform this operation is the record ID of the book's record which, of course, we keep in the BookInfo object for this very purpose:

// Deletes the entry for a book from the store
public void deleteBook(BookInfo bookInfo) throws RecordStoreException {
    if (store != null) {
        store.deleteRecord(bookInfo.id);
    }
}

Summary

The second part of this article has shown you how to use the RecordStore class in the javax.microedition.rms package to provide persistent storage for the data managed by a MIDlet. Although the facilities provided are somewhat primitive, you have seen that it is quite straightforward to implement a wrapper class that can offer higher-level operations to application code.

A couple of final observations on this application and the current state of the storage APIs for J2ME itself:

First, the javax.microedition.rms package only supports access to data created by and belonging to the calling MIDlet suite. However, some developments that will change this situation are likely to arrive later this year. The next version of MIDP, for example, will extend the RecordStore class so that a MIDlet suite can allow other suites to access its data. More significantly, the PDA profile includes the ability for Java code to access some of the native personal information on the device that is currently available only to native applications. This might, for example, include an address book, a calendar, or a to-do list. None of these are currently accessible to MIDP applications and, unfortunately, it seems that there is no plan to incorporate this feature into MIDP version 2.0, with the result that only code running on the higher-end platforms will be able to access these databases.

Secondly, the technique used in this application to obtain information from the Amazon Web site is very brittle, because it relies heavily on the format of the Web page that is returned from the server. In fact, this format has changed a couple of times since I wrote the first version of this code, and the application code had to change both times. The reason for this is, of course, that the data returned from the server is really intended for a human user, not for an application. That said, there are many other applications that use the same "screen-scraping" technique, including most of the stock-price ticker applications and MIDlets that are commonly available. In reality, this type of application should be created using a business-to-business interface, encoded in XML, with a formal specification of the services available and how to invoke them. This specification then becomes a contract between the service provider and the client and is therefore not subject to the type of change that breaks the bookstore client. What I have just described is, of course, the classic definition of a Web service. Work is already under way to define the way in which J2ME clients will access Web services. When this work is complete, perhaps it will be possible to build a version of this application that uses a formal interface to the Amazon Web server -- or other booksellers' sites -- to obtain its data.

Kim Topley has more than 25 years experience as a software developer and was one of the first people in the world to obtain the Sun Certified Java Developer qualification.


Return to ONJava.com.