J 2 M E T E C H T I P S TIPS, TECHNIQUES, AND SAMPLE CODE WELCOME to the Java Developer Connection(sm) (JDC) Java(tm) 2 Platform, Micro Edition (J2ME(tm)) Tech Tips, for January 31, 2002. This issue covers: * Maintaining Client State Across HTTP Requests * Using Fixed Point Arithmetic in the Connected Limited Device Configuration The J2ME Tech Tips are written by Eric Giguere (http://www.ericgiguere.com), an engineer at iAnywhere Solutions, inc. Eric is the author of the book "Java 2 Micro Edition: Professional Developer's Guide" and co-author of the book "Mobile Information Device Profile for Java 2 Micro Edition," both books in John Wiley & Sons' Professional Developer's Guide series. You can view this issue of the J2ME Tech Tips on the Web at http://java.sun.com/jdc/J2METechTips/2002/tt0131.html - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MAINTAINING CLIENT STATE ACROSS HTTP REQUESTS Due to the constraints of the J2ME platform, many J2ME applications offload some of their work to a server. A natural way for the client to communicate with the server is to follow the HTTP protocol. There are two good reasons for this. First, HTTP is a widely recognized and supported protocol. The Mobile Information Device Profile (MIDP), for example, includes HTTP support through the HttpConnection interface. Second, HTTP lets you do more than just fetch web pages. You can also invoke server-side code written as servlets or JavaServer Pages(tm) (JSP(tm)). This makes HTTP a natural and familiar way to communicate with Java(tm) 2 Platform, Enterprise Edition (J2EE(tm)) servers. One problem with HTTP is that it was designed specifically as a stateless protocol. In other words, each HTTP request-response cycle is independent and isolated. The server potentially keeps client connections open only as long as necessary to service a single request. This allows a Web server to service many more clients than would be possible if each client kept an open connection to the server. But it also means that the server cannot identify which requests come from which clients based solely on the connection. Instead, the clients send some form of identification to the server with each request. The server can then track which request comes from which client, and create a session for each client. Tracking clients this way is referred to as session tracking. The simplest kind of session tracking occurs when the client sends a unique identifier along with each request. For example: String URL = "http://www.mysite.com/servlet/MyApp"; int id = ...; // get the ID somewhere HttpConnection conn = (HttpConnection) Connector.open( URL + "&id=" + id ); The identifier can be sent as a request parameter or as an HTTP header. The servlet or JSP extracts the value from the request to identify the client. But where does the unique identifier come from? It can't be a randomly-generated value, since there's no guarantee it won't conflict with a number generated by another client. The name (or userid) of the user is a possibility, but only if combined with other information. This additional identification is needed in case the user runs the same application on two or more devices in overlapping time periods. A better solution is to let the server generate a unique identifier for each session. The server passes this identifier back to the client as part of the response to the client's first request. The client is then responsible for passing it back to the server on each subsequent request. This is in fact how Web browsers and Web servers do session tracking. It's usually referred to as cookie-based session tracking because it involves the exchange of a "cookie" between the two sides. A cookie is a short string of encoded data. It's easy to use cookie-based session tracking in J2ME applications. When a Web server replies to a request, it usually includes additional information in the headers of the HTTP response. If cookies are enabled, the response includes one or more Set-Cookie headers. For example, if you connect to a Web application on a Tomcat server and the response that comes back includes a cookie definition, the Set-Cookie header looks something like this: Set-Cookie: JSESSIONID=12ae8%33dk22KKLdk39jKK9;path=/ A Set-Cookie header consists of several parts, each separated by a semicolon. Most of these parts are optional. You can find the full specification on Netscape's site at http://home.netscape.com/newsref/std/cookie_spec.html. For J2EE servers, the cookie is always named "JSESSIONID", but for other servers it varies -- see the specification for the exact details. The client extracts the cookie and stores it. Subsequent requests by the client back to server should include a Cookie header, like this: Cookie: JSESSIONID=12ae8%33dk22KKLdk39jKK9 The server then knows which session to associate with the request. (As an aside, a useful tool to see raw HTTP responses returned by Web servers is found at http://www.rexswain.com/httpview.html.) Here's an example of a simple servlet that counts the number of times it has been accessed by a particular client: import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; /** * A simple servlet that increments a counter each * time a request is made. The counter is stored * as a session attribute. */ public class Counter extends HttpServlet { public static final String BINARY = "application/octet-stream"; public static final String HTML = "text/html"; public void doGet( HttpServletRequest request, HttpServletResponse response ) throws IOException, ServletException { // Get the client session. If none exists, // this creates one. The server will // automatically add a Set-Cookie header to // the servlet output. HttpSession session = request.getSession( true ); // Get the old counter value, if it exists. // Otherwise create a new one initialized to 0. Integer counter = (Integer) session.getValue( "counter" ); if( counter == null ){ counter = new Integer( 0 ); } // Increment the counter and store the new // value back in the session. counter = new Integer( counter.intValue() + 1 ); session.putValue( "counter", counter ); // If client is a J2ME device, send counter // back as a simple integer, otherwise wrap // it in an HTML page for viewing with a // conventional web browser. String ua = request.getHeader( "User-Agent" ); if( ua.indexOf( "CLDC" ) != -1 ){ ByteArrayOutputStream bout = new ByteArrayOutputStream(); DataOutputStream dout = new DataOutputStream( bout ); dout.writeInt( counter.intValue() ); dout.close(); byte[] data = bout.toByteArray(); response.setContentType( BINARY ); response.setContentLength( data.length ); OutputStream out = response.getOutputStream(); out.write( data ); out.close(); } else { response.setContentType( HTML ); PrintWriter out = response.getWriter(); out.write( "" ); out.write( counter.toString() ); out.write( "" ); out.close(); } } } Compile and install this on a servlet-enabled Web server, mapping it to the path "/counter". For example, if you use the Tomcat Web server, place the class file in the ROOT web application's WEB-INF\classes directory (create the directory if necessary). Then add the following lines to the WEB-INF\web.xml file: Counter Counter Counter /counter Use a Web browser to invoke the servlet -- for Tomcat surf to http://localhost:7080/counter -- and press the browser's refresh button several times. You should see the counter value increment. The servlet generates HTML output unless the User-Agent header sent with the response includes the string "CLDC", in which case, it writes the counter value directly to the output stream as a raw integer. Here's the code for a simple MIDlet that makes multiple requests to the servlet shown above. The response to the first request is scanned for a Set-Cookie header, and the cookie is then used on all subsequent requests: import java.io.*; import javax.microedition.lcdui.*; import javax.microedition.midlet.*; import javax.microedition.io.*; /** * A class that repeatedly makes requests to * the same URL, displaying a counter value * sent back by the web server. */ public class CounterTest extends MIDlet { private Display display; private Command exitCommand = new Command( "Exit", Command.EXIT, 1 ); private Command cancelCommand = new Command( "Cancel", Command.CANCEL, 1 ); // The hardcoded URL -- adjust appropriately. private String URL = "http://localhost:7080/counter"; // Standard MIDlet stuff.... public CounterTest(){ } protected void destroyApp( boolean unconditional ) throws MIDletStateChangeException { exitMIDlet(); } protected void pauseApp(){ } protected void startApp() throws MIDletStateChangeException { if( display == null ){ initMIDlet(); } } private void initMIDlet(){ display = Display.getDisplay( this ); display.setCurrent( new Tester() ); } public void exitMIDlet(){ notifyDestroyed(); } // Utility routine to display exceptions. void displayError( Throwable e ){ System.out.println( e.toString() ); exitMIDlet(); } // Utility routine to split a string into two. static String[] split( String in, char ch ){ String[] result = new String[2]; int pos = in.indexOf( ch ); if( pos != -1 ){ result[0] = in.substring( 0, pos ).trim(); result[1] = in.substring( pos+1 ).trim(); } else { result[0] = in.trim(); } return result; } // Simple list used to display the counter values. // Spawns a thread that repeatedly connects to a // web server and reads a counter value encoded in // the response. class Tester extends List implements Runnable, CommandListener { private HttpConnection conn; private boolean done; Tester(){ super( "Counter Test", Choice.IMPLICIT ); addCommand( cancelCommand ); setCommandListener( this ); Thread t = new Thread( this ); t.start(); } // Here we either exit or else set the // flag to cancel the HTTP operation. public void commandAction( Command c, Displayable d ){ if( c == exitCommand ){ exitMIDlet(); } else { done = true; removeCommand( cancelCommand ); } } // Do the connection and processing on // a separate thread... public void run(){ int count = 20; HttpConnection conn = null; String cookie = null; while( !done && count-- > 0 ){ try { conn = (HttpConnection) Connector.open( URL ); // Set required headers conn.setRequestProperty( "User-Agent", "Profile/MIDP-1.0 Configuration/CLDC-1.0" ); conn.setRequestProperty( "Content-Language", "en-US" ); // If we have a cookie, send it if( cookie != null ){ conn.setRequestProperty( "Cookie", cookie ); } // Connect, then read cookie value // if we don't have one already int rc = conn.getResponseCode(); if( cookie == null ){ cookie = readCookie( conn ); } // Read counter value InputStream in = conn.openInputStream(); DataInputStream din = new DataInputStream( in ); Integer counter = new Integer( din.readInt() ); append( counter.toString(), null ); in.close(); } catch( IOException e ){ displayError( e ); } if( conn != null ){ try { conn.close(); } catch( IOException e ){ displayError( e ); } } } removeCommand( cancelCommand ); addCommand( exitCommand ); } // Simple routine that iterates through the // response headers and looks for a Set-Cookie // header. It then splits the header value // into parts and looks for a cookie value // with the name "JSESSIONID", which is what // a J2EE server will use for session tracking. // Adapt this code appropriately for other // kinds of cookies. String readCookie( HttpConnection conn ) throws IOException { String key; String value; String[] substrs; for( int i = 0; ( key = conn.getHeaderFieldKey( i ) ) != null; ++i ){ key = key.toLowerCase(); if( key.equals( "set-cookie" ) ){ value = conn.getHeaderField( i ); while( value != null ){ substrs = split( value, ';' ); if( substrs[0].startsWith( "JSESSIONID=" ) ){ return substrs[0]; } value = substrs[1]; } } } return null; } } } When run, this MIDlet displays the counter value received on each request. The value should increment by one on each request. If you remove the code that sets the Cookie header and rerun the application, you'll see that the value doesn't increment. That's because a new session is being created for each request. Note that the actual HTTP connections are always done on a separate thread to ensure that the user interface remains responsive. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - USING FIXED POINT ARITHMETIC IN THE CONNECTED LIMITED DEVICE CONFIGURATION The Connected Limited Device Configuration (CLDC) defines the most basic Java environment for very constrained devices. It does this mostly by removing support for certain Java language features right in the virtual machine (VM). One of the things left out by the CLDC 1.0 specification is support for floating point operations. In other words, the float and double primitive types, as well as java.lang.Float and java.lang.Double, and any related operations, are not found in a CLDC-compliant VM. The next generation of the CLDC, currently being defined through the Java Community Process as JSR-139 (see http://www.jcp.org/jsr/detail/139.jsp), might reintroduce floating point support, but for now floating point support is out. This also applies to any profiles built on top of the CLDC, including the Mobile Information Device Profile. The lack of floating point support can seem crippling at first, especially for business applications. After all, how else can you represent non-integer values such as currency amounts? It's not nearly as bad as it seems, though, because there is a simple alternative: use fixed point numbers instead of floating point numbers. To create a fixed point number, take an integer and fix an imaginary decimal point within it. For example, the integer 3000 can represent any of the values 3000, 300.0, 30.00, 3.000, .3000, and so on. The number of digits to the right of the decimal point is the number's scale. The total number of digits on both sides of the decimal point is its precision. Both values are limited by the range of the underlying integer. Consider an expense tracking application for US-based users. Although the application displays amounts in dollar units, internally it stores the amounts in penny units. In other words, 1.56 dollars is stored as 156 pennies: a fixed point value with a scale of 2. Using CLDC-defined classes, you could write some conversion routines: // Routines to convert from pennies (ints) // to dollars (strings) and back. public class Converter { // Given an amount in pennies (an as int) // convert to dollar representation (as a string) public static String penniesToDollars( int pennies ){ StringBuffer buf = new StringBuffer(); buf.append( pennies / 100 ); buf.append( '.' ); // Ensure always two digits int frac = Math.abs( pennies ) % 100; if( frac < 10 ){ buf.append( '0' ); } buf.append( frac ); return buf.toString(); } // Given an amount in dollars (as a string) // convert to pennies (as an int). Truncates // the value after the decimal point to two digits // if necessary. public static int dollarsToPennies( String dollars ) throws NumberFormatException { int pos = dollars.indexOf( '.' ); if( pos == -1 ){ // no pennies.... return Integer.parseInt( dollars ) * 100; } else { String before = dollars.substring( 0, pos ); int val = Integer.parseInt( before ) * 100; String after = dollars.substring( pos+1 ); int len = after.length(); if( len > 2 ){ // truncate to 2 digits after = after.substring( 0, 2 ); len = 2; } if( len != 0 ){i int dec = Integer.parseInt( after ); if( len == 2 ){ val += dec; } else if( len == 1 ){ val += dec * 10; } } return val; } } } Whenever the user enters a dollar amount, convert it to pennies for internal storage and manipulation: int pennies = Converter.dollarsToPennies( "1.56" ); // pennies now set to 156 Do the reverse to display the pennies as dollars: String dollars = Converter.penniesToDollars( 156 ); // dollars now set to "1.56" Here is a simple example of a MIDlet that uses the Converter class to convert a dollar-and-cents amount into pennies and back. Two numeric input fields are used to prompt the user for the amount. The user first enters the dollars, followed by the cents. An alternative approach is to use a single unconstrained text field for the dollar-and-cents amount, but then it's harder to enter numbers on MIDP devices that use keypads. import java.io.*; import javax.microedition.lcdui.*; import javax.microedition.midlet.*; import javax.microedition.io.*; /** * A simple MIDlet that prompts for a monetary amount * in two parts and then converts the value to a fixed * point representation and back. */ public class FixedPointTest extends MIDlet { private Display display; private String dollars; private String cents; private Command exitCommand = new Command( "Exit", Command.EXIT, 1 ); private Command okCommand = new Command( "OK", Command.OK, 1 ); // Standard MIDlet stuff.... public FixedPointTest(){ } protected void destroyApp( boolean unconditional ) throws MIDletStateChangeException { exitMIDlet(); } protected void pauseApp(){ } protected void startApp() throws MIDletStateChangeException { if( display == null ){ initMIDlet(); } } private void initMIDlet(){ display = Display.getDisplay( this ); display.setCurrent( new EnterDollars() ); } public void exitMIDlet(){ notifyDestroyed(); } // Ask for dollar part of amount class EnterDollars extends TextBox implements CommandListener { EnterDollars(){ super( "Enter dollars:", "", 9, TextField.NUMERIC ); addCommand( exitCommand ); addCommand( okCommand ); setCommandListener( this ); } public void commandAction( Command c, Displayable d ){ if( c == exitCommand ) exitMIDlet(); dollars = getString(); display.setCurrent( new EnterCents() ); } } // Ask for cents part of amount class EnterCents extends TextBox implements CommandListener { EnterCents(){ super( "Enter cents:", "", 2, TextField.NUMERIC ); addCommand( exitCommand ); addCommand( okCommand ); setCommandListener( this ); } public void commandAction( Command c, Displayable d ){ if( c == exitCommand ) exitMIDlet(); cents = getString(); display.setCurrent( new ShowConversion() ); } } // Display the converted amounts class ShowConversion extends Form implements CommandListener { ShowConversion(){ super( "Result" ); String val = dollars + "." + cents; try { StringBuffer buf = new StringBuffer(); buf.append( "The amount: " ); buf.append( val ); buf.append( "\nconverts to: " ); int amt = Converter.dollarsToPennies( val ); buf.append( amt ); buf.append( "\nand then back to: " ); buf.append( Converter.penniesToDollars( amt ) ); text.setText( buf.toString() ); } catch( NumberFormatException e ){ text.setText( "Exception parsing value" ); } append( text ); addCommand( exitCommand ); addCommand( okCommand ); setCommandListener( this ); } public void commandAction( Command c, Displayable d ){ if( c == exitCommand ) exitMIDlet(); display.setCurrent( new EnterDollars() ); } private StringItem text = new StringItem( "", null ); } } You can generalize the Converter code to deal with more than two digits after the decimal point. This could be useful if you need to calculate and display sales tax values or other amounts where it's important to be able to round up or down. The Converter code above simply truncates any digits in the fractional part beyond the first two digits. Basic arithmetic operations with fixed point numbers are simple, providing both numbers use the same scale. Addition and subtraction is just a matter of adding or subtracting two integers. For example: 30.00 added to 20.00 is really just 3000 + 2000 = 5000, that is, 50.00. Multiplication is more complicated because the scale of the result is twice the scale of the two operands. For example, 0.20 multiplied by 0.20 yields 0.0400. After multiplication, then, you must adjust the scale of the result, rounding up or down as necessary. Similar comments apply to division. As always, it's important to be aware of overflow issues when dealing with any kind of arithmetic. The extended range of the long data type (-9223372036854775808 to 9223372036854775807) is usually the best choice. However, for something like an expense tracking application, the range of the int data type (-2147483648 to 2147483647) might be adequate if the scale is small enough and if numbers are only added or subtracted. If you are porting an application that makes extensive use of floating point numbers, or if you're developing an application that requires trigonometric or logarithmic functions, the MathFP library could be of interest. Free for non-commercial use, the MathFP library defines a single class, net.jscience.math.MathFP, that exposes static methods for performing arithmetic with fixed pointer numbers of variable scale. See the jScience Technologies web site (http://www.jscience.net) for further details and the MathFP download. . . . . . . . . . . . . . . . . . . . . . . . IMPORTANT: Please read our Terms of Use, Privacy, and Licensing policies: http://www.sun.com/share/text/termsofuse.html http://www.sun.com/privacy/ http://developer.java.sun.com/berkeley_license.html * FEEDBACK Comments? Send your feedback on the J2ME Tech Tips to: jdc-webmaster@sun.com * SUBSCRIBE/UNSUBSCRIBE - To subscribe, go to the subscriptions page, (https://softwarereg.sun.com/registration/developer/en_US/subscriptions), choose the newsletters you want to subscribe to and click "Update". - To unsubscribe, go to the subscriptions page, (https://softwarereg.sun.com/registration/developer/en_US/subscriptions), uncheck the appropriate checkbox, and click "Update". - To use our one-click unsubscribe facility, see the link at the end of this email: - ARCHIVES You'll find the J2ME Tech Tips archives at: http://java.sun.com/jdc/J2METechTips/index.html - COPYRIGHT Copyright 2002 Sun Microsystems, Inc. All rights reserved. 901 San Antonio Road, Palo Alto, California 94303 USA. This document is protected by copyright. For more information, see: http://java.sun.com/jdc/copyright.html J2ME Tech Tips January 31, 2002 Sun, Sun Microsystems, Java, Java Developer Connection, J2ME, J2EE, JavaServer Pages, and JSP are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries.