MIDP Provisioning With Servlets

By Eric Giguere

Download the source

In this tech tip I'll show you how to write a simple servlet that automatically selects the right version of your application and delivers it through the magic of over-the-air (OTA) provisioning.

One of the realities of mobility programming today is that developers must cope with a wide variety of devices with widely different capabilities. Whether major or minor, those differences must be handled cleanly. Sometimes the only solution is to prepare different application descriptors for different device models, or even different application versions entirely. You can trust users to select the right version to download and install -- or, following this tip, you can spare them the risk they'll get the wrong one.

Automatic provisioning is actually pretty easy to do if you're basing your application on the Mobile Information Device Profile (MIDP) and you know a bit about HTTP. When a browser requests any resource from a web site, it sends a number of headers along with the request. One of these is the User-Agent header, which identifies the "user agent," in this case the browser. There's no standard for what goes into the header, but in general it includes the browser name and version as well as the operating system name and version. Some browsers even send details about the device, such as its screen resolution. You can mine this information, including the values of other HTTP headers, to build a provisioning servlet like the one I present here.

You start by defining a Java properties file that maps files -- JADs and JARs in this case -- to devices based on simple pattern-matching of the User-Agent header value. Here's a very simple set of mappings:

    # Device definitions
    device[nokia]=Nokia
    device[mot]=MOT-

    # Nokia-specific
    nokia[MyApp.jad]=MyApp_Nokia.jad
    nokia[MyApp.jar]=MyApp_Nokia.jar

    # Motorola-specific
    mot[MyApp.jad]=MyApp_Motorola.jad
    mot[MyApp.jad]=MyApp_Motorola.jar

    # Default mapping
    default[MyApp.jad]=MyApp_Generic.jad
    default[MyApp.jar]=MyApp_Generic.jar

According to these mappings, a User-Agent value containing Nokia is assumed to be a Nokia device while MOT- indicates a Motorola device. The pattern matching could be more specific, referring to particular versions of devices, as encoded in the User-Agent header. The rest of the properties map requests for MyApp.jad and MyApp.jar into device-specific versions.

To make the automatic version selection work, you build a servlet that's contained in a very simple web application. I called mine the Provisioner. Its web.xml file looks like this:

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app 
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" 
    "http://java.sun.com/dtd/web-app_2_3.dtd">
    
<web-app>
    <display-name>provisioner</display-name>
    
    <servlet>

        <servlet-name>provisioner</servlet-name>
	<servlet-class>provisioner.Provisioner</servlet-class>
	<init-param>
	    <param-name>provisioner-config</param-name>
	    <param-value>provisioner.props</param-value>
	</init-param>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>provisioner</servlet-name>

	<url-pattern>/</url-pattern>
    </servlet-mapping>
    
</web-app>

The servlet is fairly straightforward. On startup, it reads the configuration file, by default provisioner.props, that it finds in the web app's installation directory. Requests to the servlet include the desired JAD or JAR file as part of the URL. For example:

http://www.somecompany.com/provisioner/MyApp.jad

The servlet extracts the file information from the URL. It then determines which device is making the request by matching the device patterns in the configuration file against the User-Agent header. Based on these two pieces of information -- the requested file and the device identifier -- it streams the appropriate file down to the device. Very simple but also very flexible. The servlet also supports commands for rereading the configuration file -- to let you make changes without restarting the web app -- and dumping information about the servlet. The source code that follows is a very bare-bones implementation that you can enhance in many ways. Download the JAR..

    package provisioner;

    import java.io.\*;
    import java.util.\*;
    import javax.servlet.\*;
    import javax.servlet.http.\*;

    /\*\*
     \* A simple JAD/JAR provisioning servlet can serve
     \* different files based on the user-agent header.
     \* The servlet is configured with a properties file.
     \*/

    public class Provisioner extends HttpServlet {

        /\*\*
         \* Processes the HTTP GET request. Looks for
         \* the two special commands "info" and "refresh" first,
         \* otherwise assumes that the path after the servlet path
         \* is the name of a .jad or .jar to find.
         \*/

        public void doGet( HttpServletRequest request,
                           HttpServletResponse response )
                           throws IOException, ServletException {

            String filename = request.getServletPath();

            // Quit right away if initialization failed or the
            // requested file doesn't make sense...

            if( state != STATE_RUNNING ){
                dumpState( true, request, response );
               return;
            } else if( filename == null || filename.length() < 2 ){
                showHelp( response );
                return;
            }

            String cmd = filename.substring( 1 );

            if( cmd.equals( CMD_REFRESH ) ){
                dumpState( !readConfig(), request, response );
            } else if( cmd.equals( CMD_INFO ) ){
                dumpState( false, request, response );
            } else {
                if( !cmd.endsWith( JAD_EXT )
                    && !cmd.endsWith( JAR_EXT ) ){
                    dumpState( true, request, response );
                   return;
                }

                File file = matchFile( cmd, request );
                if( file == null || !file.exists() ){
                    dumpState( true, request, response );
                    return;
                }
    
                streamFile( file, response ); 
           }
        }

        /\*\*
         \* Extracts the device name from a property of the
         \* form "device[NAME]" where NAME is the device name.
         \* Returns null on error.
         \*/

        private String extractDeviceName( String n ){
            if( n == null || !n.startsWith( DEVICE_PREFIX ) ||
                !n.endsWith( DEVICE_SUFFIX ) ) return null;

            n = n.substring( DEVICE_PREFIX.length(),
                             n.length() - DEVICE_SUFFIX.length() );
            return n.length() != 0 ? n : null;
        }

        /\*\*
         \* Initializes the provisioner by loading and parsing
         \* the configuration file.
         \*/

        public void init( ServletConfig config )
                          throws ServletException {

            servletContext = config.getServletContext();

            // Load the name of the config file

            String param = config.getInitParameter( CONFIG_PARAM );
            if( param != null ){
                configPath = param;
            }

            if( !readConfig() ){
                state = STATE_INVALID_PATH; // file not found
            } else if( devices.size() == 0 ){
                state = STATE_INVALID_CONFIG;
            } else {
                state = STATE_RUNNING;
            }
        }

        /\*\*
         \* Finds the actual file that matches the given path based
         \* on the user-agent header sent. If there is no match,
         \* returns the default file.
         \*/
 
        private File matchFile( String filename,
                                HttpServletRequest request )
                                throws IOException, ServletException {
            String   devname = DEFAULT_DEVICE;
            String   ua = request.getHeader( "user-agent" );
            Iterator it = devices.iterator();

           // Search for a matching device definition based
           // on the user-agent header

           if( ua != null ){
               while( it.hasNext() ){
                   String key = (String) it.next();
                   String fragment = props.getProperty( key );

                    if( ua.indexOf( fragment ) != -1 ){
                        devname = key;
                        break;
                    }
                }
            }

            // Now search for a matching file

            String key = extractDeviceName( devname ) + '[' + filename + ']';
            String path = props.getProperty( key );

            if( path == null ){
                path = props.getProperty( DEFAULT_NAME +
                                         '[' + filename + ']' );
            }

            if( path != null ){
                path = servletContext.getRealPath( path );
            } else {
                path = servletContext.getRealPath( filename );
            }

            return path != null ? new File( path ) : null;
        }

        /\*\*
         \* Reads the configuration file.
         \*/

        private boolean readConfig(){
            error = null;
            props.clear();

            try {
                String path = servletContext.getRealPath( configPath );
                InputStream in = new FileInputStream( path );
                if( in != null ){
                    props.load( in );
                    in.close();
                }
            }
            catch( Throwable e ){
                error = e;
                return false;
            }

            return readDevices();
        }

        /\*\*
         \* Builds the list of known devices based on the properties
         \* of the form "device[NAME]".
         \*/

        private boolean readDevices(){
            devices.clear();

            Iterator it = props.keySet().iterator();
            while( it.hasNext() ){
                String key = (String) it.next();
                if( extractDeviceName( key ) == null ) continue;
                devices.add( key );
            }

            Collections.sort( devices );
            return true;
        }

        /\*\*
         \* Show some basic help
         \*/

        private void showHelp( HttpServletResponse response )
                               throws IOException, ServletException {
            response.setStatus( response.SC_OK );
            response.setContentType( "text/plain" );

            PrintStream out =
                new PrintStream( response.getOutputStream() );
            out.println( "Provisioner Example\\n" );
            out.println(
                "A provisioning servlet that demonstrates how to" );
            out.println(
                "serve different files based on the User-Agent" );
            out.println( "header.\\n" );
            out.println(
                "See provisioner.props files for the configuration.\\n" );
            out.println( "The following commands are supported:" );
            out.println( "    info -- shows configuration details" );
            out.println( "    refresh -- rereads the configuration file" );
            out.println(
                "To invoke, just access <host>/provisioner/<command>" );
            out.close();
        }

        /\*\*
         \* Streams a JAD or JAR file to the client.
         \*/

        private void streamFile( File file, HttpServletResponse response )
                                 throws IOException, ServletException {

            response.setStatus( response.SC_OK );
            response.setContentType( file.getPath().endsWith( JAD_EXT ) ?
                                     JAD_MIME : JAR_MIME );
            response.setContentLength( (int) file.length() );

            FileInputStream fis = new FileInputStream( file );
            BufferedInputStream bis = new BufferedInputStream( fis );
            OutputStream out = response.getOutputStream();
            int ch;

            while( ( ch = bis.read() ) != -1 ){
                out.write( ch );
            }

            bis.close();
            out.close();
        }

        /\*\*
         \* Dumps the current state back to the client
         \* as a simple text file that is also viewable
         \* by a standard web browser.
         \*/

        private void dumpState( boolean notFound,
                                HttpServletRequest request,
                                HttpServletResponse response )
                                throws IOException, ServletException {

            response.setStatus( notFound ?
                                response.SC_NOT_FOUND :
                                response.SC_OK );
            response.setContentType( "text/plain" );

            PrintStream out = new PrintStream(
                response.getOutputStream() );
            out.println( "Provisioner Example\\n" );
            out.println( "State:  " + state );
            out.println( "Config: " + configPath );
            out.println( "HTTP response code " +
                         ( notFound ?
                           response.SC_NOT_FOUND :
                           response.SC_OK ) );

            if( notFound ){
                out.println( "File was not found" );
            }

            out.println( "" );

            out.println( "ContextPath: " + request.getContextPath() );
            out.println( "PathInfo:    " + request.getPathInfo() );
            out.println( "RequestURI:  " + request.getRequestURI() );
            out.println( "ServletPath: " + request.getServletPath() );

            if( devices.size() > 0 ){
                out.println(
                    "\\nMatching is done in the following order:" );
                Iterator it = devices.iterator();
                while( it.hasNext() ){
                    out.println( (String) it.next() );
                }
            }

            if( props.size() > 0 ){
                props.list( out );
            }

            if( error != null ){
                out.println( "\\nException thrown:" );
                error.printStackTrace( out );
            }

            out.close();
        }

        private String     configPath = CONFIG_FILE;
        private List       devices = new ArrayList();
        private Throwable  error = null;
        private Properties props = new Properties();
        private ServletContext servletContext;
        private String     state = STATE_UNINITIALIZED;

        // The info command

        private static final String CMD_INFO = "info";

        // The refresh command

        private static final String CMD_REFRESH = "refresh";

        // The default config file name

        private static final String CONFIG_FILE = "provisioner.props";

        // The default config initialiation parameter

        private static final String CONFIG_PARAM = "provisioner-config";

        // The prefix for the device definition

        private static final String DEVICE_PREFIX = "device[";

        // The suffix for the device definition

        private static final String DEVICE_SUFFIX = "]";

        // The default device name

        private static final String DEFAULT_NAME = "default";

        // The default device property

        private static final String DEFAULT_DEVICE = DEVICE_PREFIX +
                                                     DEFAULT_NAME +
                                                     DEVICE_SUFFIX;

        // The JAD extension

        private static final String JAD_EXT = ".jad";

        // The JAD MIME type

        private static final String JAD_MIME =
            "text/vnd.sun.j2me.app-descriptor";

        // The JAR extension

        private static final String JAR_EXT = ".jar";

        // The JAR MIME type

        private static final String JAR_MIME =
            "application/java-archive";

        // The bad config state

        private static final String STATE_INVALID_CONFIG =
            "Invalid configuration";

        // The bad path state

        private static final String STATE_INVALID_PATH =
            "Configuration path invalid";

        // The running state

        private static final String STATE_RUNNING = "Running";

        // The uninitialized state

        private static final String STATE_UNINITIALIZED =
            "Not initialized";
    }
Comments:

trtr

Posted by guest on November 22, 2007 at 09:02 PM PST #

Sorry, I don't understand your comment. Please expand.

Posted by Christine Dorffi on November 26, 2007 at 01:27 AM PST #

Hi,
How do I create a WAR file and deploy this in a jboss environment?
Thx.

Posted by Dillon J on March 18, 2008 at 03:27 PM PDT #

Post a Comment:
  • HTML Syntax: NOT allowed
About

Tips for developers who use Java technologies (Java SE, Java ME, JavaFX) for mobile and embedded devices.

Search

Categories
Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today