/*
J2ME in a Nutshell
By Kim Topley
ISBN: 0-596-00253-X
*/
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import javax.microedition.rms.RecordComparator;
import javax.microedition.rms.RecordEnumeration;
import javax.microedition.rms.RecordFilter;
import javax.microedition.rms.RecordListener;
import javax.microedition.rms.RecordStore;
import javax.microedition.rms.RecordStoreException;
import java.util.Vector;
import javax.microedition.lcdui.Alert;
import javax.microedition.lcdui.AlertType;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.List;
import javax.microedition.lcdui.Screen;
import javax.microedition.lcdui.StringItem;
import javax.microedition.lcdui.TextField;
import javax.microedition.midlet.MIDlet;
import javax.microedition.midlet.MIDletStateChangeException;
import javax.microedition.rms.RecordEnumeration;
import javax.microedition.rms.RecordListener;
import javax.microedition.rms.RecordStore;
import javax.microedition.rms.RecordStoreException;
public class PersistentRankingMIDlet extends MIDlet
implements CommandListener, RecordListener, Runnable {
private Command exitCommand;
private Command okCommand;
private Command cancelCommand;
private Command newCommand;
private Command checkCommand;
private Command detailsCommand;
private Command backCommand;
private Command deleteCommand;
private Display display;
private TextField isbnField;
private StringItem isbnDisplay;
private StringItem titleDisplay;
private StringItem rankingDisplay;
private StringItem reviewDisplay;
private StringItem checkTitle;
private Form isbnForm;
private Form searchForm;
private Form resultForm;
private Form checkForm;
private List bookList;
private Vector bookInfoList;
private Thread searchThread;
private BookStore bookStore;
private BookInfo searchBookInfo;
protected void startApp() throws MIDletStateChangeException {
if (display == null) {
initialize();
// If there are any books in the
// book store, display the list.
// Otherwise, start with the ISBN form.
display.setCurrent(getSelectionScreen());
}
}
protected void pauseApp() {
}
protected void destroyApp(boolean unconditional)
throws MIDletStateChangeException {
// Close the book store
if (bookStore != null) {
try {
bookStore.removeRecordListener(this);
bookStore.close();
} catch (RecordStoreException ex) {
}
}
}
public void commandAction(Command cmd, Displayable d) {
if (cmd == exitCommand) {
try {
destroyApp(true);
} catch (MIDletStateChangeException ex) {
}
notifyDestroyed();
} else if (cmd == okCommand) {
String isbn = isbnField.getString().trim();
if (!isbn.equals("")) {
isbnDisplay.setText(isbn);
display.setCurrent(searchForm);
searchForBook(new BookInfo(isbn));
}
} else if (cmd == cancelCommand) {
searchThread = null;
isbnField.setString(null);
display.setCurrent(getSelectionScreen());
} else if (cmd == newCommand) {
isbnField.setString(null);
display.setCurrent(isbnForm);
} else if (cmd == detailsCommand || cmd == List.SELECT_COMMAND) {
int index = bookList.getSelectedIndex();
searchBookInfo = (BookInfo)bookInfoList.elementAt(index);
isbnDisplay.setText(searchBookInfo.getIsbn());
showBookInfo(searchBookInfo);
} else if (cmd == deleteCommand) {
int index = bookList.getSelectedIndex();
BookInfo bookInfo = (BookInfo)bookInfoList.elementAt(index);
try {
bookStore.deleteBook(bookInfo);
} catch (RecordStoreException ex) {
System.out.println("Delete failed: " + ex);
}
} else if (cmd == checkCommand) {
String isbn = searchBookInfo.getIsbn();
checkTitle.setText(searchBookInfo.getTitle());
display.setCurrent(checkForm);
searchForBook(searchBookInfo);
} else if (cmd == backCommand) {
display.setCurrent(getSelectionScreen());
}
}
public void searchForBook(BookInfo info) {
searchBookInfo = info;
searchThread = new Thread(this);
searchThread.start();
}
public void recordAdded(RecordStore recordStore, int recordId) {
// Update the book list
populateBookList();
}
public void recordChanged(RecordStore recordStore, int recordId) {
// Update the book list
populateBookList();
}
public void recordDeleted(RecordStore recordStore, int recordId) {
// Update the book list
populateBookList();
}
public void run() {
try {
boolean found = Fetcher.fetch(searchBookInfo);
if (searchThread == Thread.currentThread()) {
if (found && searchBookInfo.getTitle() != null) {
// Display the book details
showBookInfo(searchBookInfo);
// Add the new book to the book store
bookStore.saveBookInfo(searchBookInfo);
} else {
Alert alert = new Alert("Book not found", null,
null, AlertType.ERROR);
alert.setTimeout(Alert.FOREVER);
alert.setString("No book with ISBN " +
searchBookInfo.getIsbn() +
" was found.");
isbnField.setString(null);
display.setCurrent(alert, getSelectionScreen());
}
}
} catch (Throwable ex) {
if (searchThread == Thread.currentThread()) {
Alert alert = new Alert("Search Failed", null,
null, AlertType.ERROR);
alert.setTimeout(Alert.FOREVER);
alert.setString("Search failed:\n" + ex.getMessage());
isbnField.setString(null);
display.setCurrent(alert, getSelectionScreen());
}
}
}
// Shows book details on the result screen
private void showBookInfo(BookInfo info) {
titleDisplay.setText(info.getTitle());
int ranking = info.getRanking();
int lastRanking = info.getLastRanking();
int change = ranking - lastRanking;
String rankingText =
ranking == 0 ? "" :
String.valueOf(ranking);
if (change > 0) {
rankingText += ", down by " + change;
} else if (change < 0) {
rankingText += ", UP by " + (-change);
}
rankingDisplay.setText(rankingText);
int reviews = info.getReviews();
int lastReviews = info.getLastReviews();
change = reviews - lastReviews;
String reviewText =
reviews == 0 ? "" :
String.valueOf(reviews);
if (change > 0) {
reviewText += ", up by " + change;
}
reviewDisplay.setText(reviewText);
display.setCurrent(resultForm);
}
// If there are any books in the
// book store, display the list.
// Otherwise, start with the ISBN form.
private Screen getSelectionScreen() {
return
bookInfoList.isEmpty() ? (Screen)isbnForm : (Screen)bookList;
}
// Populates the list of books
private void populateBookList() {
// Clear out any existing content
int count = bookList.size();
for (int i = 0; i < count; i++) {
bookList.delete(0);
}
bookInfoList.removeAllElements();
// Add an entry for each book in the store
try {
RecordEnumeration e = bookStore.getBooks();
while (e.hasNextElement()) {
int id = e.nextRecordId();
BookInfo info = bookStore.getBookInfo(id);
bookInfoList.addElement(info);
String title = info.getTitle();
if (title == null || title.equals("")) {
title = info.getIsbn();
}
bookList.append(title, null);
}
e.destroy();
} catch (Exception ex) {
// Just leave an empty list.
}
// The ISBN list should have an exit command
// only if the List screen is empty.
isbnForm.removeCommand(exitCommand);
if (bookInfoList.isEmpty()) {
isbnForm.addCommand(exitCommand);
}
}
private void initialize() {
display = Display.getDisplay(this);
// Open the book store
bookStore = new BookStore();
exitCommand = new Command("Exit", Command.EXIT, 0);
okCommand = new Command("OK", Command.OK, 0);
cancelCommand = new Command("Cancel", Command.CANCEL, 0);
newCommand = new Command("New", Command.SCREEN, 1);
detailsCommand = new Command("Details", Command.OK, 0);
checkCommand = new Command("Check", Command.OK, 0);
backCommand = new Command("Back", Command.CANCEL, 1);
deleteCommand = new Command("Delete", Command.SCREEN, 1);
bookList = new List("Books", List.IMPLICIT);
bookList.addCommand(detailsCommand);
bookList.addCommand(newCommand);
bookList.addCommand(deleteCommand);
bookList.addCommand(exitCommand);
bookInfoList = new Vector();
isbnForm = new Form("Book Query");
isbnForm.append("Enter an ISBN and press OK:");
isbnField = new TextField("", null, 10, TextField.ANY);
isbnForm.append(isbnField);
isbnForm.addCommand(okCommand);
isbnForm.addCommand(exitCommand);
isbnForm.addCommand(backCommand);
searchForm = new Form("Book Search");
searchForm.append("Searching for ISBN\n");
isbnDisplay = new StringItem(null, null);
searchForm.append(isbnDisplay);
searchForm.append("\nPlease wait....");
searchForm.addCommand(cancelCommand);
checkForm = new Form("Details Update");
checkForm.append("Getting details for\n");
checkTitle = new StringItem(null, null);
checkForm.append(checkTitle);
checkForm.append(" from amazon.com\nPlease wait....");
checkForm.addCommand(cancelCommand);
resultForm = new Form("Search Results");
titleDisplay = new StringItem("Book title: ", null);
rankingDisplay = new StringItem("Ranking: ", null);
reviewDisplay = new StringItem("Reviews: ", null);
resultForm.append(titleDisplay);
resultForm.append(rankingDisplay);
resultForm.append(reviewDisplay);
resultForm.addCommand(backCommand);
resultForm.addCommand(checkCommand);
// Register for events from all of the forms
isbnForm.setCommandListener(this);
searchForm.setCommandListener(this);
resultForm.setCommandListener(this);
bookList.setCommandListener(this);
// Listen for changes in the content of the book store
bookStore.addRecordListener(this);
// Install the books held in the record store
populateBookList();
}
}
// A class that implements a persistent store
// of books, keyed by ISBN.
class BookStore implements RecordComparator, RecordFilter {
// The name of the record store used to hold books
private static final String STORE_NAME = "BookStore";
// The record store itself
private RecordStore store;
// ISBN to be used during a filter operation
private String searchISBN;
// Creates a bookstore and opens it
public BookStore() {
try {
store = RecordStore.openRecordStore(STORE_NAME, true);
} catch (RecordStoreException ex) {
System.err.println(ex);
}
}
// Closes the bookstore
public void close() throws RecordStoreException {
if (store != null) {
store.closeRecordStore();
}
}
// Gets the number of books in the book store
public int getBookCount() throws RecordStoreException {
if (store != null) {
return store.getNumRecords();
}
return 0;
}
// Adds a listener to the book store
public void addRecordListener(RecordListener l) {
if (store != null) {
store.addRecordListener(l);
}
}
// Removes a listener from the book store
public void removeRecordListener(RecordListener l) {
if (store != null) {
store.removeRecordListener(l);
}
}
// 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;
}
// Gets a BookInfo from a store record
// given its ISBN
public BookInfo getBookInfo(String isbn) throws RecordStoreException,
IOException {
BookInfo bookInfo = null;
searchISBN = isbn;
// Look for a book with the given ISBN
RecordEnumeration e = store.enumerateRecords(
this, null, false);
// If found, get its identifier and
// fetch its BookInfo object
if (e.numRecords() > 0) {
int id = e.nextRecordId();
bookInfo = getBookInfo(id);
}
// Release the enumeration
e.destroy();
return bookInfo;
}
// Gets a BookInfo from a store record
// given its record identifier
public BookInfo getBookInfo(int id) throws RecordStoreException,
IOException {
byte[] bytes = store.getRecord(id);
DataInputStream is = new DataInputStream(
new ByteArrayInputStream(bytes));
String isbn = is.readUTF();
BookInfo info = new BookInfo(isbn);
info.id = id;
info.title = is.readUTF();
info.ranking = is.readInt();
info.reviews = is.readInt();
info.lastRanking = is.readInt();
info.lastReviews = is.readInt();
return info;
}
// 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) {
searchISBN = bookInfo.getIsbn();
RecordEnumeration e = store.enumerateRecords(
this, null, false);
if (e.numRecords() > 0) {
// A matching record exists. Set the id
// of the BookInfo to match the existing record
bookInfo.id = e.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
e.destroy();
}
}
// Deletes the entry for a book from the store
public void deleteBook(BookInfo bookInfo) throws RecordStoreException {
if (store != null) {
store.deleteRecord(bookInfo.id);
}
}
// 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;
}
}
// 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;
}
// Writes a record into a byte array.
private byte[] toByteArray(BookInfo bookInfo) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream os = new DataOutputStream(baos);
os.writeUTF(bookInfo.isbn);
os.writeUTF(bookInfo.title == null ? "" : bookInfo.title);
os.writeInt(bookInfo.ranking);
os.writeInt(bookInfo.reviews);
os.writeInt(bookInfo.lastRanking);
os.writeInt(bookInfo.lastReviews);
return baos.toByteArray();
}
}
/**
* A class that represents a book listing
* at an online book set, including the number
* of reviews for the book and its sales ranking.
*/
class BookInfo {
int id; // Used when persisting
String isbn; // The book ISBN
String title; // The book title
int reviews; // Number of reviews
int ranking; // Current ranking
int lastReviews; // Last review count
int lastRanking; // Last ranking
public BookInfo(String isbn) {
this.isbn = isbn;
}
public String getIsbn() {
return isbn;
}
public String getTitle() {
return title;
}
public int getReviews() {
return reviews;
}
public int getRanking() {
return ranking;
}
public int getLastReviews() {
return lastReviews;
}
public int getLastRanking() {
return lastRanking;
}
// Installs details parsed from an input stream
public void setFromInputStream(InputStream is) {
// Use an InputHelper to search the input
InputHelper helper = new InputHelper(is);
try {
// Default new values to current values
int newRanking = this.ranking;
int newReviews = this.reviews;
boolean found = helper.moveAfterString("buying info: ");
if (!found) {
return;
}
// Gather the title from the rest of this line
StringBuffer titleBuffer = helper.getRestOfLine();
// Look for the number of reviews
found = helper.moveAfterString("Based on ");
if (!found) {
return;
}
// Gather the number of reviews from the current location
String reviewString = helper.gatherNumber();
// Look for the sales rank
found = helper.moveAfterString("Sales Rank: ");
if (!found) {
return;
}
// Gather the number from the current location
String rankingString = helper.gatherNumber();
// Having safely found everything, set the new title
title = titleBuffer.toString().trim();
// Now convert the reviews and ranking to integers.
// If they fail to convert, just leave the existing
// values.
try {
newRanking = Integer.parseInt(rankingString);
} catch (NumberFormatException ex) {
}
if (newRanking != ranking) {
lastRanking = ranking;
ranking = newRanking;
if (lastRanking == 0) {
// First time, set last and current
// to the same value
lastRanking = ranking;
}
}
try {
newReviews = Integer.parseInt(reviewString);
} catch (NumberFormatException ex) {
}
if (newReviews != reviews) {
lastReviews = reviews;
reviews = newReviews;
if (lastReviews == 0) {
// First time, set last and current
// to the same value
lastReviews = reviews;
}
}
} catch (IOException ex) {
} finally {
// Allow garbage collection
helper.dispose();
helper = null;
}
}
}
// A class that scans through an input
// stream for strins without reading the
// entire stream into a large string.
class InputHelper {
// Size of the input buffer
private static final int BUFFER_SIZE = 1024;
// The input buffer
private final char[] buffer = new char[BUFFER_SIZE];
// Number of characters left in the buffer
private int charsLeft;
// Index of the next character in the buffer
private int nextChar;
// InputStreamReader used to map to Unicode
private InputStreamReader reader;
// Constructs a helper to read a given stream
public InputHelper(InputStream is) {
reader = new InputStreamReader(is);
}
// Cleans up when no longer needed
public void dispose() {
if (reader != null) {
try {
reader.close();
} catch (IOException ex) {
}
reader = null;
}
}
// Looks for a given string in the input
// stream and positions the stream so that the
// next character read is one beyond the string.
// Returns true if the string was found, false if
// not (and the stream will have been completely read).
public boolean moveAfterString(String str) throws IOException {
char[] chars = str.toCharArray();
int count = chars.length;
char firstChar = chars[0];
char c = (char)0;
for (;;) {
if (c != firstChar && !findNext(firstChar)) {
// Reached the end of the input stream
return false;
}
boolean mismatch = false;
for (int i = 1; i < count; i++) {
c = getNext();
if (c != chars[i]) {
mismatch = true;
break;
}
}
if (!mismatch) {
return true;
}
// Mismatch. 'c' has the first mismatched
// character - start the loop again with
// that character. This is necessary because we
// could have found "wweb" while looking for "web"
}
}
// Gets the characters for a number, ignoring
// the grouping separator. The number starts at the
// current input position, but any leading non-numerics
// are skipped.
public String gatherNumber() throws IOException {
StringBuffer sb = new StringBuffer();
boolean gotNumeric = false;
for (;;) {
char c = getNext();
// Skip until we find a digit.
boolean isDigit = Character.isDigit(c);
if (!gotNumeric && !isDigit) {
continue;
}
gotNumeric = true;
if (!isDigit) {
if (c == '.' || c == ',') {
continue;
}
break;
}
sb.append(c);
}
return sb.toString();
}
// Gets the balance of the current line
// and returns it as a StringBuffer
public StringBuffer getRestOfLine() throws IOException {
StringBuffer sb = new StringBuffer();
char c;
for (;;) {
c = getNext();
if (c == '\n' || c == (char)0) {
break;
}
sb.append(c);
}
return sb;
}
// Gets the next character from the stream,
// returning (char)0 when all input has been read.
private char getNext() throws IOException {
if (charsLeft == 0) {
charsLeft = reader.read(buffer, 0, BUFFER_SIZE);
if (charsLeft < 0) {
return (char)0;
}
nextChar = 0;
}
charsLeft--;
return buffer[nextChar++];
}
// Finds the next instance of a given character in the
// input stream. The input stream is positioned after
// the located character. If EOF is reached without
// finding the character, false is returned.
private boolean findNext(char c) throws IOException {
for (;;) {
if (charsLeft == 0) {
charsLeft = reader.read(buffer, 0, BUFFER_SIZE);
if (charsLeft < 0) {
return false;
}
nextChar = 0;
}
charsLeft--;
if (c == buffer[nextChar++]) {
return true;
}
}
}
}
|