A J2ME Library and a Simple HTTP Service Framework
Join the DZone community and get the full member experience.
Join For FreeJ2ME's support for calling a server is rather simple and low level. Not
only do you have to deal with the HTTP Connection at a low level, there
is no high level support for cookies, authentication or remote
procedure calling. So if you want to pass an object to the server and
get a response, you need to figure out how to do that. XML over HTTP is
one solution, but presents its own problems like the serialisation and
deserialisation of objects, not to mention higher network traffic
because of the meta-data held within the XML. JAX Binding is certainly
not supported in J2ME-land which results in you having to use a SAX
parser.
In previous projects I have toyed with a simple way of
providing services over JSPs, which take and receive delimited text.
The idea is to implement your own simple serialization and
deserialisation of simple objects allowing you to make simple calls to
the server and receive simple responses. I purposefully used the word
"simple" four times in that last sentence to impress upon you the idea
that server calls should be kept simple.
Take for example a J2ME application which tracks a GPS
location. To send the location of the user it can simply send a line of
text like this:
006.574438|045.453345|11022344843373
What's it mean?
longitude | latitude | timestamp
The serialising and deserialising of the data is VERY simple
using a StringTokenizer (erm, which doesn't exist in J2ME, so see
later!). And the server could respond with a simply OK or it might want
to take the opportunity to update the client with some traffic
information:
M4|4|6|20|Accident
which means:
road | from junction | to junction | how many minutes delay | reason
Most server calls really can be that simple, especially when
being involved in J2ME applications which tend to be relatively simple
themselves.
So the above presents a few questions... How secure is the
data and how do you know who the update is coming from? Well the data
should be sent over SSL to ensure that its secure and if the data is
sent over an authenticated session, then the server knows who the
logged in user is. But to get a log in and session to work, you need
two things, namely cookies (for the session ID to be passed between
client and server) and some form of authentication. Cookies in J2ME
aren't too easy to handle since there is no built in API for handling
them at a high level. You can set a cookie in the request header, but
storing cookies from responses is the hard part. I implemented
rudimentary cookie handling by sending the response to a method which
checks for any set-cookie headers and adds them to the cache as well as
cleaning out expired entries. When a request is sent, I call a method
which adds all relevant cookies to the request header. I have not
implemented the RfC for cookies
and don't handle the differences between Cookie and Cookie2. In fact I
didn't even go as far as checking the path of the cookie before sending
it to the server, because in my environment, it's not even relevant. A
proper cookie implementation would need to do those things and more,
and perhaps one day, such a library will exist.. I did manage to find this which refers to the JCookie sourceforge project and J2ME, but checking out its site I couldn't find anything that would work with J2ME.
HTTP authentication I originally handled by adding the "authorization"
request header and applying a Base64 encoded basic authentication
string to it. This caused its own problems, because J2ME doesn't have a
Base64 encoder out of the box. Luckily the
org.apache.common.codecs.binary.Base64 class works (which I took from
the Apache Codecs 1.3 library. It depends upon the following classes
which are in the distributable JAR and also J2ME compatible:
BinaryDecoder, BinaryEncoder, Decoder, DecoderException, Encoder,
EncoderException, all found in the org.apache.commons.codec package.
I ran into a different problem when I wanted my web
application for the web site to be the same as the web application for
my services. Namely, for services I wanted to use basic authentication
and for the web site I wanted to use form login. The servlet spec
doesn't let you define more than one login-config per web-app! So the
J2ME class I had written for accessing services (ServerCaller) had to
be extended, but that wasn't hard. When a web application using Form
Login needs you to authenticate, it simply returns the login form
instead of the expected response. If you parse the response and check
to see if it is your login form, and then simply submit that form with
your username and password, the server then logs you in and returns a
302 code telling you to call the original page again. Assuming you
provided the correct login credentials, it all works. So my class
recursed into itself if it spotted the login form and that was enough
to make it work transparently.
The next problem was the parsing of the responses in order to
deserialise them. For this I created a very simple StringTokenizer,
since neither CLDC nor MIDP gives you one :-( The implementation is
below.
URL encoding and decoding is also important because request
parameters (for a GET its the request string, for a POST the request
body). Luckily there are some options out there. Catalina has one in
its util package, which almost works for J2ME. So I did a quick fix to
it to remove the dependency on java.util.BitSet which also doesn't
exist in J2ME.
The last problem I had related to chunking of the request by the HTTPConnection, which you can read more about here.
So, finally to some code! The magic class is the ServerCaller
which you can download below. It is abstract and to use it you simply
need to extend it, for example:
package uk.co.maxant.cam.c;
import java.util.Vector;
import uk.co.maxant.cam.m.MasterData;
import uk.co.maxant.j2me.core.CredentialsProvider;
import uk.co.maxant.j2me.core.ServerCaller;
import uk.co.maxant.j2me.core.StringTokenizer;
/**
* calls the server to get master data like roles and the version of the latest available download.
*/
public abstract class MasterDataGetter extends ServerCaller implements ServerCaller.Listener {
private ExceptionHandler eh;
public MasterDataGetter(CredentialsProvider credentialsProvider, ExceptionHandler eh) {
super(credentialsProvider, Constants.getBaseUrl(), "ota/getMasterData.jsp");
this.eh = eh;
setListener(this);
}
public void callServer(){
properties.clear();
properties.put("installUid", String.valueOf(InstallHelper.getInstallationUid(eh)));
doCallAsync();
}
protected void handleSuccess(String str) {
StringTokenizer lines = new StringTokenizer(str, "|");
String version = lines.nextToken().trim();
String sessionId = lines.nextToken().trim();
String screenName = lines.nextToken().trim();
String roles = lines.nextToken().trim();
StringTokenizer rs = new StringTokenizer(roles, ",");
Vector rolesv = new Vector();
while(rs.hasMoreTokens()){
rolesv.addElement(rs.nextToken());
}
String s = lines.nextToken().trim();
boolean isSendDebugLog = s.equalsIgnoreCase("true");
MasterData masterData = new MasterData(version, rolesv, sessionId, screenName, isSendDebugLog);
onSuccess(masterData );
}
}
An example of a JSP for the server is:
<%@page import="uk.co.maxant.cam.web.VersionHelper"%>
<%@page import="org.apache.log4j.Logger"%>
<%@page import="java.io.FileInputStream"%>
<%@page import="java.util.Properties"%>
<%@page import="java.io.RandomAccessFile"%>
<%@page import="java.io.File"%>
<%@page import="uk.co.maxant.util.ServiceLocator"%>
<%@page import="uk.co.maxant.cam.services.OrchestrationService"%>
<%@page import="uk.co.maxant.cam.data.Party"%>
<%@page import="java.util.List"%>
<%@page import="uk.co.maxant.cam.services.OrchestrationService.MasterData"%>
<%
Logger log = Logger.getLogger(this.getClass().getCanonicalName());
String user = "";
String roles = "";
//either they are logged in, or we can check their login against the authentication header, since its ALWAYS sent
try{
//we know who they are. if the installUid is known, lets add it to their profile!
String installUid = request.getParameter("installUid");
if(installUid != null && installUid.equals("null")) installUid = null;
String auth = request.getHeader("authorization");
OrchestrationService os = ServiceLocator.getInstance().getOrchestrationService();
MasterData md = os.getMasterData(auth, installUid);
if(md.party != null) user = md.party.getScreenname();
for(String role : md.roles){
roles += role + ",";
}
String version = VersionHelper.getVersionFromJad(request);
boolean sendDebugLog = true; //change to false if we get swamped. but really, we shouldnt ever get this stuff.
%>OK
<%=version%>|
<%=session.getId()%>|
<%=user%>|
<%=roles%>|
<%=sendDebugLog%>|
<%
}catch(Exception e){
log.warn("failed to read master data", e);
%>ERROR
<%=e.getMessage() %>
<%
}
%>
The result, is the maxant J2ME library, which runs on CLDC 1.1 and MIDP 2.0, and can be downloaded here. It includes classes for all of the following, and more, and is released under the LGPL Licence:
- URLEncoder - from Apache Catalina
- Base64 - from Apache Commons Codec
- CredentialsProvider - an interface which the ServerCaller uses to get server credentials for logging in
- Formatter - formats for example decimals nicely
- GAUploader - loads an event to Google Analytics
- Math - some math stuff thats not in CLDC
- Point - a 2 dimensional point in a plane
- ServerCaller - a high level API for calling a server, as described above
- StringTokenizer - simple, and a lot like that from J2SE
- URL - an encapsulation of a protocol, domain, port and page
- ImageHelper - code taken from the web for scaling images
- DirectoryChooser - a UI for choosing a local directory on the device
- FileChooser - a UI for choosing a local file on the device
To use the library, add it to the "lib" folder of your J2ME project,
then ensure its on the build path, and in Eclipse Pulsar, you need to
also check the box in the build path on the last tab, to ensure the
classes in the JAR are exported to your JADs JAR.
Opinions expressed by DZone contributors are their own.
Comments