System Admins and Developers
|
|
By Qusay Mahmoud
Release 1.0
December 2000
Source: QuotesMIDlet.java, Stock.java, StockDB.java
Persistent storage is a non-volatile place for storing the state of objects. For some applications, you might need objects to exist even after the application that created those objects quits. Without persistent storage, objects and their states are destroyed when an application closes. If you save objects to persistent storage, their lifetime is longer than the program that created them, and later you can read their state and continue to work with them.
The persistent storage facilities provided in the Java 2 Standard Edition (J2SE) platform, such as the JDBC and Object Serialization APIs, are not suitable for handheld devices with a small memory footprint. This is because the storage requirements vary significantly from one resource-constrained device to another. For example, a MIDlet that lets you buy (add to your portfolio) and sell (delete from your portfolio) stocks through your cell phone needs a place to store the stock database.
This article introduces the details of the MIDP Record Management System (RMS), a persistent storage for MIDlets, and shows how to develop MIDP database applications, using a stock database example. Throughout this article the terms record store and database are used interchangeably.
Introducing the RMS
The MIDP provides a mechanism for MIDlets to persistently store data and retrieve it later. This mechanism is a simple record-oriented database called the Record Management System (RMS). A MIDP database (or a record store) consists of a collection of records that remain persistent after the MIDlet exits. When you invoke the MIDlet again, it can retrieve data from the persistent record store.
To use the RMS, import the javax.microedition.rms package.
Introducing the Record Store
Record stores (binary files) are platform-dependent because they are created in platform-dependent locations. MIDlets within a single application (a MIDlet suite) can create multiple record stores (database files) with different names. The RMS APIs provide the following functionality:
Record Store Names
Record store names are case sensitive, and cannot be more than 32 characters long. Also, a MIDlet cannot create two record stores with the same name in the same application, but it can create a record store with the same name as a MIDlet in another application. When you create a new record store, it is stored under a directory called NOJAM. For example, assume you are using the Wireless Toolkit and that it is is installed under C:\J2MEWTK. If your project name is StockQuotes and your record store is mystocks, the record store is created under C:\J2MEWTK\NOJAM and it has the name mystocks.db.
Working with Threads
The MIDP RMS implementation ensures that all individual record store operations are atomic, synchronous, and serialized, so no corruption occurs with multiple access. However, if your MIDlets use multiple threads to access a record store, it is your responsibility to synchronize this access, or some of your records might be overwritten.
The RMS Package
The RMS package consists of the following four interfaces, one class, and five exception classes:
Interfaces
RecordComparator: Defines a comparator to compare two records. RecordEnumeration: Represents a bidirectional record enumerator. RecordFilter: Defines a filter to examine a record and checks if it matches based on a criteria defined by the application. RecordListener: Receives records which were added, changed, or deleted from a record store.Classes
RecordStore: Represents a record store.
Exceptions
InvalidRecordIDException: Thrown to indicate the RecordID is invalid. RecordStoreException: Thrown to indicate a general exception was thrown. RecordStoreFullException: Thrown to indicate the record store file system is full. RecordStoreNotFoundException: Thrown to indicate the record store could not be found. RecordStoreNotOpenException: Thrown to indicate an operation on a closed record store.Programming with the RMS
Database programming with RMS is relatively straightforward. This section covers the essential RecordStore methods, and if you want to learn about its other methods, see the javax.microedition.rms APIs.
What is a Record Store?
A record store consists of a collection of records that are uniquely identified by their record ID, which is an integer value. The record ID is the primary key for the records. The first record has an ID of 1, and each additional record is assigned an ID that is the previous value plus 1.
Opening a Record Store
To open a record store, use the openRecordStore() static method:
RecordStore db = RecordStore.openRecordStore("myDBfile", true);
The above code creates a new database file named myDBfile. The second parameter, which is set to true, says that if the record store does not exist, create it.
Note: If the openRecordStore() method is called by a MIDlet when the record store is already open by another MIDlet in the same application, the method returns a reference to the same RecordStore object.. |
||||
Creating a New Record
A record is an array of bytes. You can use the DataInputStream, DataOutputStream, ByteArrayInputStream, and ByteArrayOutputStream classes to pack and unpack data types into and out of the byte arrays. The first record created has an ID of 1 and is the primary key. The second record has the previous ID + 1.
Now suppose you have the following string record: FirstName, LastName, Age. To add this record to the record store, use the addRecord() method as follows:
ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos); dos.writeUTF(record); byte[] b = baos.toByteArray(); db.addRecord(b, 0, b.length);
You construct a DataOutputStream for writing the record to the record store, then you convert the ByteArrayOutputStream to a byte array, and finally you invoke addRecord() to add the record to the record store. Note that in this segment of code, no exceptions are handled. The stock database example discussed later shows how to handle the exceptions.
Reading Data from the Record Store
To read a record from the record store, you construct input streams instead of output streams. This is done as follows:
ByteArrayInputStream bais = new ByteArrayInputStream(record1); DataInputStream dis = new DataInputStream(bais); String in = dis.readUTF();
Deleting a Record from the Record Store
To delete a record from the record store, you have to know the record ID for the record to be deleted. To delete the record, use the deleteRecord() method. This method takes an integer as a parameter, which is the record ID of the record to be deleted.
There is no method to get the record ID. To work around this, every time you create a new record, add its record ID to a vector like this:
Vector recordIDs = new Vector(); int lastID = 1; //Add a record....parameters are missing here db.addRecord(); // Now add the ID to the vector recordIDs.addElement(new Integer(++lastID));
Now, to delete a record, find the record ID of the record you want to delete:
Enumeration IDs = recordIDs.elements(); while(IDs.hasMoreElements()) { int id = ((Integer) IDs.nextElement()).intValue(); //Compare to see if this is the record you want by //invoking compare() which is shown next. //Then call db.deleteRecord(id); }
Comparing my Record with Records in the Record Store
To search for the right record to delete, your application must implement the Comparator interface (by providing an implementation to the compare method) to compare two records. The return value indicates the ordering of the two records. For example, suppose you want to compare two strings that you retrieved from two records. Here is a sample implementation:
public someClas implements Comparator { public int compare(byte[] record1, byte[] record2) { ByteArrayInputStream bais1 = new ByteArrayInputStream(record1); DataInputStream dis1 = new DataInputStream(bais1); ByteArrayInputStream bais2 = new ByteArrayInputStream(record2); DataInputStream dis2 = new DataInputStream(bais2); String name1 = dis1.readUTF(); String name2 = dis.readUTF(); int num = name1.compareTo(name2); if (num > 0) { return RecordComparator.FOLLOWS; } else if (num < 0) { return recordcomparator.precedes; } else { return recordcomparator.equivalent; } } }
The constants FOLLOWS, PRECEDES, and EQUIVALENT are defined in the RecordComparator interface and have the following meanings:
FOLLOWS: Its value is 1 and means the left parameter follows the right parameter in terms of search or sort order. PRECEDES: Its value is -1 and means the left parameter precedes the right parameter in terms on search or sort order. EQUIVALENT: Its value is 0 and means the two parameters are the same.Closing the Record Store
To close the record store, use the closeRecordStore() method.
Example: Building a Stock Database
This example demonstrates how to work with the RMS to build a real MIDlet application. This application also builds on previous experience you have gained from the MIDP Network Programming article. This application is similar to the StockMIDlet demo that comes with the MIDP.
The MIDlet for this example does the following:
To add a stock to the database, the user enters the stock symbol (such as, SUNW, IBM, IT, MS, GM, or Ford). The MIDlet retrieves the corresponding stock quote from the Yahoo Quote Server (http://quote.yahoo.com), constructs a record, and adds the record to the database.
To view the stocks in the record store, the MIDlet iterates through the records in the record store and prints them on the display in a nice format.
The Implementation
The implementation of this MIDlet consists of the following three classes: Stock.java, StockDB.java, and QuotesMIDlet.java.
The Stock.java Class
This class parses a string obtained from the Yahoo Quote Server or the record store into fields (such as name of stock or price). The string returned from the Yahoo Quote Server has the following format:
| NAME | TIME | PRICE | CHANGE | LOW | HIGH | OPEN | PREV |
| "SUNW", | "2:1PM | - 79.75", | +3.6875, | "64.1875 - | 129.3125", | 78, | 76.0625 |
In this MIDlet, the fields retrieved are the name of the stock, the time, and the price.
Listing 1: Stock.java
public class Stock { private static String name, time, price; // Given a quote from the server, // retrieve the name, //price, and date of the stock public static void parse(String data) { int index = data.indexOf('"'); name = data.substring(++index, (index = data.indexOf('"', index))); index +=3; time = data.substring(index, (index = data.indexOf('-', index))-1); index +=5; price = data.substring(index, (index = data.indexOf('<', index))); } // get the name of the stock from // the record store public static String getName(String record) { parse(record); return(name); } // get the price of the stock from // the record store public static String getPrice(String record) { parse(record); return(price); } }
To
The StockDB.java Class
This class provides methods that do the following:
Once you understand how to open a record store, add a new record, and close the record store, this code is easy to follow.
Listing 2: StockDB.java
import javax.microedition.rms.*; import java.util.Enumeration; import java.util.Vector; import java.io.*; public class StockDB { RecordStore recordStore = null; public StockDB() {} // Open a record store with the given name public StockDB(String fileName) { try { recordStore = RecordStore.openRecordStore( fileName, true); } catch(RecordStoreException rse) { rse.printStackTrace(); } } // Close the record store public void close() throws RecordStoreNotOpenException, RecordStoreException { if (recordStore.getNumRecords() == 0) { String fileName = recordStore.getName(); recordStore.closeRecordStore(); recordStore.deleteRecordStore( fileName); } else { recordStore.closeRecordStore(); } } // Add a new record (stock) // to the record store public synchronized void addNewStock(String record) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream outputStream = new DataOutputStream(baos); try { outputStream.writeUTF(record); } catch (IOException ioe) { System.out.println(ioe); ioe.printStackTrace(); } byte[] b = baos.toByteArray(); try { recordStore.addRecord(b, 0, b.length); } catch (RecordStoreException rse) { System.out.println(rse); rse.printStackTrace(); } } // Enumerate through the records. public synchronized RecordEnumeration enumerate() throws RecordStoreNotOpenException { return recordStore.enumerateRecords( null, null, false); } }
The QuotesMIDlet.java Class
The QuotesMIDlet class is the actual MIDlet that does the following:
Listing 3: QuotesMIDlet.java
import javax.microedition.rms.*; import javax.microedition.lcdui.*; import javax.microedition.midlet.*; import javax.microedition.io.*; import java.io.*; import java.util.Vector; public class QuotesMIDlet extends MIDlet implements CommandListener { Display display = null; List menu = null; // main menu List choose = null; TextBox input = null; Ticker ticker = new Ticker("Database Application"); String quoteServer = "http://quote.yahoo.com/d/quotes.csv?s="; String quoteFormat = "&f=slc1wop"; // The only quote format supported static final Command backCommand = new Command("Back", Command.BACK, 0); static final Command mainMenuCommand = new Command("Main", Command.SCREEN, 1); static final Command saveCommand = new Command("Save", Command.OK, 2); static final Command exitCommand = new Command("Exit", Command.STOP, 3); String currentMenu = null; // Stock data String name, date, price; // record store StockDB db = null; public QuotesMIDlet() { // constructor } // start the MIDlet public void startApp() throws MIDletStateChangeException { display = Display.getDisplay(this); // open a db stock file try { db = new StockDB("mystocks"); } catch(Exception e) {} menu = new List("Stocks Database", Choice.IMPLICIT); menu.append("List Stocks", null); menu.append("Add A New Stock", null); menu.addCommand(exitCommand); menu.setCommandListener(this); menu.setTicker(ticker); mainMenu(); } public void pauseApp() { display = null; choose = null; menu = null; ticker = null; try { db.close(); db = null; } catch(Exception e) {} } public void destroyApp(boolean unconditional) { try { db.close(); } catch(Exception e) {} notifyDestroyed(); } void mainMenu() { display.setCurrent(menu); currentMenu = "Main"; } // Construct a running ticker // with stock names and prices public String tickerString() { StringBuffer ticks = null; try { RecordEnumeration enum = db.enumerate(); ticks = new StringBuffer(); while(enum.hasNextElement()) { String stock1 = new String(enum.nextRecord()); ticks.append(Stock.getName(stock1)); ticks.append(" @ "); ticks.append(Stock.getPrice(stock1)); ticks.append(" "); } } catch(Exception ex) {} return (ticks.toString()); } // Add a new stock to the record store // by calling StockDB.addNewStock() public void addStock() { input = new TextBox( "Enter a Stock Name:", "", 5, TextField.ANY); input.setTicker(ticker); input.addCommand(saveCommand); input.addCommand(backCommand); input.setCommandListener(this); input.setString(""); display.setCurrent(input); currentMenu = "Add"; } // Connect to quote.yahoo.com and // retrieve the data for a given // stock symbol. public String getQuote(String input) throws IOException, NumberFormatException { String url = quoteServer + input + quoteFormat; StreamConnection c = (StreamConnection)Connector.open( url, Connector.READ_WRITE); InputStream is = c.openInputStream(); StringBuffer sb = new StringBuffer(); int ch; while((ch = is.read()) != -1) { sb.append((char)ch); } return(sb.toString()); } // List the stocks in the record store public void listStocks() { choose = new List("Choose Stocks", Choice.MULTIPLE); choose.setTicker( new Ticker(tickerString())); choose.addCommand(backCommand); choose.setCommandListener(this); try { RecordEnumeration re = db.enumerate(); while(re.hasNextElement()) { String theStock = new String(re.nextRecord()); choose.append(Stock.getName( theStock)+" @ " + Stock.getPrice(theStock), null); } } catch(Exception ex) {} display.setCurrent(choose); currentMenu = "List"; } // Handle command events public void commandAction(Command c, Displayable d) { String label = c.getLabel(); if (label.equals("Exit")) { destroyApp(true); } else if (label.equals("Save")) { if(currentMenu.equals("Add")) { // add it to database try { String userInput = input.getString(); String pr = getQuote(userInput); db.addNewStock(pr); ticker.setString(tickerString()); } catch(IOException e) { } catch(NumberFormatException se) { } mainMenu(); } } else if (label.equals("Back")) { if(currentMenu.equals("List")) { // go back to menu mainMenu(); } else if(currentMenu.equals("Add")) { // go back to menu mainMenu(); } } else { List down = (List)display.getCurrent(); switch(down.getSelectedIndex()) { case 0: listStocks();break; case 1: addStock();break; } } } }
Testing QuotesMIDlet
To test QuotesMIDlet, which was developed using the Wireless Toolkit:
QuotesMIDlet running in the emulator as shown in Figure 1:
Figure 1: QuotesMIDlet
For more information about testing MIDlets using the Wireless Toolkit, see the Quick Start to the Wireless Toolkit
QuotesMIDlet.
You see a menu with the following two options: List Stocks and Add a New Stock, as shown in Figure 2:
Figure 2: QuotesMIDlet Stock Database
Choose the Add A New Stock option and add a few stocks.
In this example, the stocks IBM, GM, and NOR were added, as shown in Figure 3:
Figure 3: Adding new stocks
Figure 4: Viewing the record store
Going Forward
The stock quotes example demonstrates how to create new records and view the records in the database. As an exercise, consider modifying QuoteMIDlet to handle the following situations:
More Information
MIDP Network Programming using HTTP and the Connection Framework
Quick Guide to the J2ME Wireless Toolkit
Java 2 Platform, Micro Edition (J2ME)