Wi Stock PDF
Wi Stock PDF
Wi Stock PDF
Jack Wind
Author
08 Oct 2002
In this tutorial, we will build a typical Java 2 Platform, Micro Edition (J2ME)
application, called UniStocks, based on CLDC (Connected Limited Device
Configuration) and MIDP (Mobile Information Device Profile) APIs.
Section 1. Introduction
• MIDlet basics
• MIDP high-level user interface design
• MIDP low-level user interface design
• Record management system (RMS)
• J2ME networking and multithreading
• Server-side design
• Application optimization and deployment
• Overcoming J2ME limitations
About UniStocks
UniStocks is a stock application that enables the user to access and manage
information of any stock -- anywhere, anytime.
Like any stock application on your PC or on the Web, UniStocks lets the user:
Figures 1 through 3 show the main menu; the downloading status, and the stock
historical chart, respectively.
Other tools, such as IBM VisualAge Micro Edition and Borland JBuilder Mobile Set
2.0, are extensions of mature IDEs. They provide wizards and other tools to help you
create J2ME applications.
You should choose the right tools according to your needs. (See Resources for IDE
links.) For this project, we'll use the text editor Emacs with WTK 1.04.
Because those methods signal state changes, they need to be lightweight in order to
return quickly. As you can see in the above code listing, we put most of the
initialization process in <init> and the constructor, rather than in startApp().
Screen is the super class of all high-level interface APIs. Figure 4 shows a screen
map of UniStocks. Note that "Historical charts," with the gray background, uses the
low-level API to draw charts to the screen. The screen map does not show the
intermediate-level screens, such as alerts and error reporting screens.
If you really want to display a splash screen, display it only when the user first
launches the MIDlet. Users will become frustrated if they must wait for your splash
screen to display every time.
In this application, we use a simple "About" alert to show the application's nature and
license information.
In a tree structure like this, we can use the navigation techniques Depth-First-Search
and Breadth-First-Search. Further, the implementation will be easy.
class Node {
Node parent;
Vector children;
boolean isRoot;
...
}
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
/**
* SubForm: A template of all subforms, 'node' in a tree.
*
* @version $2.1 2002-JAN-15$
* @author JackWind Li Guojie (http://www.JackWind.net)
*/
public class SubForm extends Form implements CommandListener
{
Command backCommand; // Back to the parent screen.
UniStock midlet; // The MIDlet.
Displayable parent; // The parent screen.
/**
* Constructor - pass in the midlet object.
*/
public SubForm(String title, UniStock midlet, Displayable parent) {
super(title);
this.midlet = midlet;
this.parent = parent;
backCommand = new Command("Back", Command.BACK, 1);
addCommand(backCommand);
setCommandListener(this);
}
/**
* commandListener: Override this one in subclass!
* Call this method in subclass: super.commandAction(c, s)
*/
public void commandAction(Command c, Displayable s) {
if(c == backCommand) {
if(parent != null)
midlet.display.setCurrent(parent);
}
}
}
We don't keep a children list in the node because we usually create new child
screens on the fly. (Of course, you can keep a children list if you don't want to create
child screens every time.) When the user presses the Back command, the system
simply displays its parent screen. The child might make some changes on the parent
screen, and then display its parent screen after the Back button is pressed.
Using this tree model, we can easily create user-friendly J2ME applications. As an
alternative, you can look into another navigation model, called a stack-based
framework, described by John Muchow in Core J2ME Technology and MIDP . (See
Resources.)
A sample screen
The following code list is a simplified version of our "View Stock Details" form
implementation. The class FormView extends the tree node implementation
SubForm. FormView adds its own customized commands, methods, and so on. It
also overrides the commandAction() method for its customized command
event-handling routine:
import javax.microedition.lcdui.*;
/**
* Form: Display view stock form.
*
* @version 1.0 2002-JUL-07
* @author JackWind Li Guojie (http://www.JackWind.net)
*/
Command commandOneYear;
int mode; // 1 - Live info.
// 2 - One month.
// 3 - Three months.
// 4 - Six months.
// 5 - One year.
Stock s; // Selected stock.
StockLive sl;
StockHistory sh;
public FormView(String title, UniStock midlet, Displayable parent) {
super(title, midlet, parent);
commandLive = new Command("Live Info", Command.SCREEN, 1);
commandOneMonth = new Command("One m. chart", Command.SCREEN, 1);
commandThreeMonth = new Command("Three m. Chart", Command.SCREEN,
1);
commandSixMonth = new Command("Six m. Chart", Command.SCREEN, 1);
commandOneYear = new Command("One yr. Chart", Command.SCREEN, 1);
addCommand(commandLive);
addCommand(commandOneMonth);
addCommand(commandThreeMonth);
addCommand(commandSixMonth);
addCommand(commandOneYear);
choiceStocks = new ChoiceGroup("Select a stock: ",
Choice.EXCLUSIVE);
for(int i=0; i<midlet.stocks.size(); i++) {
if(UniStock.DEBUG)
UniStock.debug("Loading #" + i);
Stock s = (Stock)midlet.stocks.elementAt(i);
Exchange e = (Exchange)midlet.exchanges.elementAt((int)s.ex);
choiceStocks.append( s.code + " [" + e.code + "]", null);
}
append(choiceStocks);
}
public void commandAction(Command c, Displayable ds) {
super.commandAction(c, ds);
if(c == commandLive || c == commandOneMonth || c==
commandThreeMonth
|| c == commandSixMonth || c == commandOneYear)
{
if(c == commandLive)
mode = 1;
else if(c == commandOneMonth)
mode = 2;
else if(c == commandThreeMonth)
mode = 3;
else if(c == commandSixMonth)
mode = 4;
else if(c == commandOneYear)
mode = 5;
if(choiceStocks == null || choiceStocks.getSelectedIndex() == -1)
{
midlet.reportError("Nothing selected to view!");
s = null;
return;
}else{
s =
(Stock)(midlet.stocks.elementAt(choiceStocks.getSelectedIndex()));
}
Download dl = new Download(this, midlet);
NextForm nextForm = new NextForm(c.getLabel(), midlet, this, dl);
midlet.display.setCurrent(nextForm);
Thread t = new Thread(dl);
dl.registerListener(nextForm);
t.start();
}
}
Why use MVC or user interface delegation? With MVC and UI delegation, you can
adapt your application painlessly. In a J2ME environment, MVC lets you do modular
component testing. You can fully test the business logic code before mixing it with
the GUI part.
If we want to add another attribute to the Stock class -- for example, company
name -- the Stock constructor needs one more parameter. We also need to check
whether the user tries to add certain restricted stocks. For the MVC code, we simply
modify the addStock() method in UniStock. For the second listing, we must
modify every code snippet that contains the code for creating and/or adding stocks,
which can be tedious:
Some books use code similar to the above code to show their readers how to use
Alert. However, that code is wrong. When you press the Delete command, the
above code will run. During the execution, you might find that "Data deleted" is
printed immediately after the Alert displays (as shown here in Figure 6). If you
press the Delete command unintentionally, you cannot cancel or roll back deletion
because the data has already been deleted before you noticed it.
Alert is misused in the above code. According to the MIDP Java API
documentation, "The intended use of Alert is to inform the user about errors and
other exceptional conditions." Therefore, we need a dialog here. In the next section,
I will present a flexible, reusable Dialog class.
/**
* FileName: Dialog.java
* Version: 1.0
* Create on: 2001-JUN-01
*
* All rights reserved by JackWind Group. (www.JackWind.net)
*/
import javax.microedition.lcdui.*;
/**
* Dialog: A class simulating dialog for UI.
*
* @version 1.0 2001-JUN-01
* @author JackWind Group (http://www.JackWind.net)
*/
class DialogListener {
public void onOK() {}
public void onYES() {}
public void onNO() {}
public void onCANCEL() {}
public void onCONFIRM() {}
// RETRY.
// ABORT.
}
setCommandListener(this);
}
public void commandAction(Command c, Displayable s) {
if(dll != null) {
if(c == cmOK)
dll.onOK();
else if(c == cmYES)
dll.onYES();
else if(c == cmNO)
dll.onNO();
else if(c == cmCANCEL)
dll.onCANCEL();
else if(c == cmCONFIRM)
dll.onCONFIRM();
}
midlet.display.setCurrent(parent);
}
}
Using Dialog
Using our Dialog class, we can rewrite the code from the "What's Wrong with
Alert?" section:
display.setCurrent(dl);
Now, when you press the Delete command, you will see the screen shown here in
Figure 7. You can confirm the deletion or simply cancel this action. Similarly, you
can use this Dialog to let the user answer simple questions with YES, NO, OK, and
so on.
way, you simply override any method necessary without implementing all the
methods. In the above code, we use an anonymous inner class as
DialogListener, and override the onCONFIRM() method.
What's next
In this section, we will build a Canvas to display stock price and volume charts.
After the user selects a stock symbol and historical period, the application will
retrieve historical data from the server. (I will discuss networking later.) Getting the
necessary data, the application will draw the actual charts onto the canvas (see
below).
The user can view price and volume for every historical trading day by pressing the
right arrow to find the next day or the left arrow for the previous day. (See
information about event processing in subsequent sections.) By pressing the up and
down arrow, the user can zoom in and zoom out (see Figure 9).
Low-level drawing
When creating a Canvas, we need to extend the Canvas class and override at least
its paint(Graphics g) method. By overriding the paint() method, we can draw
the stock chart:
1, 1, Graphics.TOP | Graphics.LEFT);
g.drawString("Volume: " + sr.volumn,
1, fontH + 2, Graphics.TOP | Graphics.LEFT);
// Draw the chart.
for(int i=left+1; i<=right; i++) {
// For each visible day (except the first day).
StockRecord current = (StockRecord)sh.vec.elementAt(i);
// Draw price chart.
// Multiplication first, then division to increase accuracy.
g.setColor(255, 51, 0); // Set color
g.setStrokeStyle(Graphics.SOLID);
g.drawLine(
startX + (i-1-left)*step,
startY + Y - (last.priceHigh-priceLowest)*Y/priceBase,
startX + (i-left)*step,
startY + Y - (current.priceHigh - priceLowest)*Y/priceBase
);
// Draw volume chart.
last = current;
The above code is our paint() method. Inside the method, we get a reference to
the Graphics object; thus, we can use it to do the actual drawing. These main
drawing methods are available in Graphics:
Then we draw the price and volume chart by concatenating small segments created
by drawLine() for each period.
The origin of the graphics coordinate system is at the upper left corner, with
coordinates increasing down and to the right, as Figures 10 and 11 illustrate. The
arguments required by drawing and filling methods define a coordinate path (shown
in the gray spots in the figures) instead of pixel positions. This can sometimes be
confusing.
But Canvas can also handle low-level events. For low-level events -- such as game
action, key events, and pointer events -- we don't need to create and register
listeners, because Canvas has direct methods to handle them:
• showNotify()
• hideNotify()
• keyPressed()
• keyRepeated()
• keyReleased()
• pointerPressed()
• pointerDragged()
• pointerReleased()
• paint()
The following is our event-handling routine in StockCanvas:
Once the user presses a key, the keyPressed() method will be called with the
pressed key code as the only parameter.
ensure our application's portability. getGameAction() will translate a key code into
a game action. Those game actions should be available on all J2ME-supported
devices. However, a hand phone might have different code settings with a two-way
pager. So we need to translate those settings with getGameAction().
Tip: Use game actions, such as UP, RIGHT, and LEFT, to ensure application
portability.
Double buffering
Occasionally, you find that canvases flicker during repainting. This flickering is due
to the fact that the Canvas class must clear the previous screen (background)
before it invokes the paint() method. Erasing Canvas's background results in
flickering, which we can eliminate using a well-known technique called double
buffering.
If the implementation supports double buffering, we don't need to repeat it. Thus, we
must check it before double buffering in the paint() method.
Understanding RecordStore
Records inside a RecordStore are uniquely identified by their recordID, which is
an integer value. The first record that RecordStore creates will have a recordID
equal to 1. Each subsequent record added to RecordStore will have a recordID
one greater than the last added record.
For example, Figure 12 shows the internal state transition of our RecordStore.
State 2 does not contain any record with a recordID equal to 2 or 3. However, its
'Next ID' does not change. As you can see clearly from the state representations
below, RecordStore is not a Vector. In the following sections, you will learn how
to correctly add and retrieve records.
Controlling RecordStores
Open and create a RecordStore:
The code listing below tries to open a RecordStore. If the RecordStore identified
by its name does not exist, RMS will try to create it. The RecordStore name
should not exceed 32 characters. You should also try to avoid using duplicated
names while creating RecordStores. The openRecordStore() method throws
several exceptions, so we need to manage exception handling:
/**
* Open a record store.
*/
private RecordStore openRecStore(String name) {
try {
// Open the record store, create it if it does not exist.
return RecordStore.openRecordStore(name, true);
}catch(Exception e) {
reportError("Fail to open RS: " + name);
return null;
}
}
Close a RecordStore:
If you don't need to use a RecordStore anymore, you can simply close it to release
the resources it holds:
// Clean up.
try {
...
rsStockList.closeRecordStore();
}catch(Exception e) {
// reportError("Clean-up error:" + e.toString());
}
Erase a RecordStore:
Warning: When you delete a RecordStore, you erase its associated records!
Create records
As I mentioned earlier, a record is a byte array. However, we usually store data of
types String, int, and so on in records. Here we can use DataInputStream
and ByteArrayInputStream to pack data into records:
recordID is returned. We can use the following code to check whether or not a
new record has been created successfully:
/**
* Add a new stock.
*/
boolean addStock(String code, byte ex, String temp) {
Stock s = new Stock(code, ex);
int id = writeStock(s);
if(id > 0) {
s.rs_id = id;
stocks.addElement(s);
return true;
}else{
return false;
}
}
*/
private void readStock() {
stocks = new Vector();
try {
int total = rsStockList.getNumRecords();
if(total == 0) // No record..
return;
RecordEnumeration re = rsStockList.enumerateRecords(null, null,
false);
byteArrayInput.reset();
for(int i=0; i<total; i++) {
int id = re.nextRecordId();
if(DEBUG)
debug("Reading " + (i+1) + " of total " + total + " id = " +
id);
rsStockList.getRecord( id, recData, 0);
Stock s = new Stock (
dataInput.readUTF(), // full name - String
dataInput.readByte() // num - byte
); );
s.rs_id = id; // Keep a copy of recordID.
stocks.addElement (s);
byteArrayInput.reset();
}
}catch(Exception e) {
if(DEBUG)
e.printStackTrace();
}
}
Tip: Always keep a copy of recordIDs after loading data from the records. Those
recordIDs will be useful if you need to modify or delete records later.
eraseStock( ((Stock)stocks.elementAt(i)).rs_id )) ;
/*
* Erase a record.
* @param i recordID
*/
private boolean eraseStock(int i) {
try {
rsStockList.deleteRecord(i);
}catch(Exception e) {
reportError(e.toString());
return false;
}
return true;
Using HttpConnection
HTTP is a request-response protocol in which the request parameters must be set
before the request is sent. In UniStocks, we use the following code to retrieve a
stock's live and historical data:
try{
http = (HttpConnection) Connector.open(URL);
http.setRequestMethod(HttpConnection.GET);
if(hasListener)
dl.setProgress(1, 10);
// Query the server and try to retrieve the response
is = http.openInputStream();
if(hasListener)
dl.setProgress(2, 10);
String str; // Temp buffer.
int length = (int) http.getLength();
if(hasListener)
dl.setProgress(3, 10);
if(length != -1) { // Length available.
byte data[] = new byte[length];
is.read(data);
str = new String(data);
}else{ // Length not available.
ByteArrayOutputStream bs = new ByteArrayOutputStream();
int ch;
while((ch = is.read()) != -1)
bs.write(ch);
str = new String(bs.toByteArray());
bs.close();
}
if(UniStock.DEBUG)
UniStock.debug("Got Data:>" + str + "<");
// String downloaded.....
// Process string here.
...
if(hasListener)
dl.setProgress(10, 10);
// Alert the user.
// AlertType.INFO.playSound(midlet.display);
} catch (IOException e) {
if(midlet.DEBUG)
midlet.debug("Downloading error: " + e.toString());
if(formAdd != null) {
formAdd.stockOK = false;
}else if(formView != null) {
}
} finally {
if(formAdd != null)
formAdd.process();
if(formView != null)
; // Do something.
/// Clean up.
if(is != null)
is.close();
if(http != null)
http.close();
if(dl != null)
dl.onFinish();
}
1. Setup: In this state, the connection has not been made to the server.
http = (HttpConnection) Connector.open(URL) only creates
an HttpConnection, which is not yet connected.
3. Closed: The connection has been closed and the methods will throw an
IOException if called. In our case, we close all the data streams and
connections before exiting our customized download method.
We have two major goals: Show the user connection progress, and give the user
control. When the user presses the Download command, the system will create and
start a new thread for download. At the same time, the download screen is created
and displayed to the user:
if(c == commandOneMonth) {
mode = 1;
...
Download dl = new Download(this, midlet);
// Download screen.
NextForm nextForm = new NextForm(c.getLabel(), midlet, this, dl);
midlet.display.setCurrent(nextForm);
Thread t = new Thread(dl);
dl.registerListener(nextForm);
t.start();
}
You can use another technique: Close the connection when Cancel is pressed.
Once the connection is closed, Connection methods will throw an IOException if
called. However, any streams created from connection are still valid even if the
connection has been closed.
My development team and I have found that the first interruption technique performs
better.
Download class:
try{
http = (HttpConnection) Connector.open(URL);
http.setRequestMethod(HttpConnection.GET);
if(hasListener)
dl.setProgress(1, 10);
if(interrupted)
throw new InterruptedException();
is = http.openInputStream();
if(hasListener)
dl.setProgress(2, 10);
if(UniStock.DEBUG)
UniStock.debug("Connecting to: " + URL);
String str; // Temp buffer.
int length = (int) http.getLength();
if(hasListener)
dl.setProgress(3, 10);
if(interrupted)
throw new InterruptedException();
if(length != -1) { // Length valid.
byte data[] = new byte[length];
is.read(data);
str = new String(data);
}else{ // Length not available.
ByteArrayOutputStream bs = new ByteArrayOutputStream();
int ch;
while((ch = is.read()) != -1)
bs.write(ch);
str = new String(bs.toByteArray());
bs.close();
}
if(interrupted)
throw new InterruptedException();
if(UniStock.DEBUG)
UniStock.debug("Got Data:>" + str + "<");
// String downloaded.....
// Process data here.
...
if(hasListener)
dl.setProgress(10, 10);
// Alert the user.
// AlertType.INFO.playSound(midlet.display);
} catch (IOException e) {
if(midlet.DEBUG)
midlet.debug("Downloading error: " + e.toString());
if(formAdd != null) {
formAdd.stockOK = false;
}else if(formView != null) {
}
} finally {
if(formAdd != null)
formAdd.process();
if(formView != null)
; // Do something.
Collecting information
The server side provides us with stock information. The best way to get that stock
data is from a fast database; we can then send the data to the client. However, we
usually don't have such a convenient database to access. Our solution here is to
parse the Yahoo! Finance Web pages, which provide stock information from major
exchanges, and then forward the formatted data to the client side. You can easily
add your regional exchanges in the same way.
You may directly process the Web pages on the client side. However, once the Web
pages change their format, you have to update all installed MIDlets. If you later have
a faster way to access the stock data, you could simply update the server side only.
Tip: You are not limited to use Java language in server-side programming. Since the
client side does not care about the server-side implementation, you can use your
favorite script languages, such as Perl, ColdFusion, and so forth.
Symbol Current Price, Date, Time, Change Open High Low Volume
"IBM", 76.50, "8/15/2002", "4:00pm", +1.58, 75.40,76.71,74.60, 9269600
If the MIDlet requests a certain stock's live information, it must supply the stock's
symbol to query the server. The server then queries the Yahoo! Finance server to
get information. The server should return the following information to the MIDlet:
[Status * last price * price change * open price * high * low * volume]
1 * 73200 *1020 *73750 *73990 *73070*4586500*
Similarly, the server could retrieve the historical data from Yahoo! Finance and send
it to the client. Here is the sample output (historical data):
/**
* getString: Form a String from a big integer.
* We assume the integer has been multiplied by 1000.
*/
public static final String getString(int i, boolean trimZero, boolean
isPositive) {
if(isPositive && i < 0) {
return " N/A";
}
if(i==0) return "0";
boolean neg = (i > 0 ? false : true);
String str= Integer.toString(i);
if(neg) {
i = -i; // Make it positive first.
str = str.substring(1);
}
if(i < 10) { // Like 9.
str = "0.00" + str;
}else if(i < 100) { // Like 98.
str = "0.0" + str;
}else if(i < 1000) { // Like 450.
str = "0." + str;
}else{ // Like 10900.
str = Integer.toString(i);
str = str.substring(0, str.length()-3) + "." +
str.substring(str.length()-3);
}
if(neg)
str = "-" + str;
if(trimZero) { // Trim extra zeros
int dotP = str.indexOf('.');
int pos = -1;
if(dotP < 0) // if no '.' is found.
return str;
for(pos = str.length()-1; pos >= dotP; pos --)
if(str.charAt(pos) != '0')
break;
if(pos == dotP)
// If nothing is left behind '.', remove '.' too.
pos -= 1;
return str.substring(0, pos+1);
}
return str;
}
Hardcoding technique
My development team and I developed a shooting game recently. During
development, my colleagues complained that J2ME has no sin, cos, or tg methods.
Without knowing those sin and cos values, shooting would be uncontrollable. Finally,
we identified that we only had 15 different angles, so we hardcoded their sin and cos
values into the program. You could use this hardcoding technique to solve similar
problems.
If you use the WTK, you can see your JAD file's attributes by pressing the Settings
button. The first tab displays the required attributes, some of which you need to
modify:
If you installed the StockInfoProvider servlet on your Web server, you may want
to set this URL pointing to the servlet's exact address.
Section 9. Wrap up
What we covered
In this tutorial, we have covered almost every aspect of J2ME by developing a
typical application -- UniStocks.
You received great hands-on experience in MIDLet basics, high-level user interface
4. Set up your own stock provider (optional). First configure your proxy host
and proxy port in file StockInfoProvider.java (inside the jackwind
folder). Compile source file and install the servlet. For details, please refer
to README.txt under jackwind/WEB-INF. Set the UniStocks JAD file's
user-defined attribute INFO_PROVIDER_URL to your servlet URL; for
example:
http://127.0.0.1:8080/jackwind/servlet/StockInfoProvider
Resources
Learn
• Create your J2ME application with IBM VisualAge Micro Edition.
• Check out Borland JBuilder Mobile Set 2.0 as an IDE alternative.
• Sun One Studio 4.0 EA Mobile Edition is a free, feature-rich IDE.
• Visit Sun's wireless developer homepage.
• View JSR 30: J2ME Connected, Limited Device Configuration.
• View JSR 37: Mobile Information Device Profile for the J2ME Platform.
• To know more about multithreading with Java, read Java Thread Programming,
David M. Geary (Sams, 1999).
• To know more about patterns, read Design Patterns: Elements of Reusable
Object-Oriented Software, Richard Helm, et al. (Addison-Wesley, 1994).
• A must-read for every user interface designer: The Design of Everyday Things,
Donald Norm (MIT Press, 1998).
• For more information on the stack-based framework, read John Muchow's Core
J2ME Technology and MIDP (Prentice Hall/Sun Microsystems Press, 2001).
• For more information, visit Wireless zone on IBM developerWorks.
• Visit the author's Web site http://www.jackwind.net for more J2ME resources
and services.
Get products and technologies
• Download the UniStocks binary and source from http://www.jackwind.net.
• Download the Java 2 Platform, Micro Edition, Wireless Toolkit.