Monday 8 February 2010

Java, JDBC & SOAP on Windows Mobile 2003 Pocket PC

Symbol MC50

Check it out! The Symbol MC50 looks smart enough to divide by zero and tough enough to club a seal to death with! Here is how to program it!

This post is about getting Java running on Pocket PC, setting up an Apache Derby embedded database, and using kSOAP 2 to communicate with a web service.

ActiveSync

First off you're going to need to use ActiveSync to create a guest partnership between your Pocket PC and your development machine. If your running Windows XP, get ActiveSync v4.5 from here.

Java ME

Sun and Microsoft don't do a JVM for Pocket PC/Windows Mobile 2003, so a 3rd party JVM is the way to go. There are a few options but I'm having a punt on WebSphere Everyplace Micro Environment (WEME).

You can grab a trial version of WebSphere Everyplace Micro Environment Personal Profile 1.0 for Windows Mobile 2003 from here, or here

Run the EXE file contained in the downloaded WEME ZIP to install the JRE onto your local machine and onto the Pocket PC via ActiveSync.

Java ME JDBC

To use JDBC on the Pocket PC with WEME you will need The JDBC Optional Package for CDC-Based J2ME Applications. The source code is available for download here, or is available as a Maven build at the bottom of this page.

The JDBC optional package Maven build produces a 'jdbc-cdc-1.0.jar' JAR file. Copy the JAR to the Pocket PC JRE folder:

\Program Files\J9\PPRO10\lib\jclPPro10\ext

Web Service

Next knock out a quick SOAP web service for the Pocket PC to access. Below is a simple ColdFusion component which has a getStockPrice method that returns a price for a given stock name:

<cfcomponent namespace="http://www.example.org/stock">

  <cffunction access="remote" returnType="numeric" name="getStockPrice">
    <cfargument required="true" type="string" name="stockName" >
  
    <cfset var stockPrice = -1>
    
    <cfswitch expression="#Trim(ARGUMENTS.stockName)#">      
      <cfcase value="IBM">
        <cfset stockPrice = 123.91>
      </cfcase>

      <cfcase value="MSFT">
        <cfset stockPrice = 28.25>
      </cfcase>

      <cfcase value="ADBE">
        <cfset stockPrice = 32.74>
      </cfcase>

      <cfcase value="ORCL">
        <cfset stockPrice = 23.43>
      </cfcase>      
    </cfswitch>
  
    <cfreturn stockPrice>
  </cffunction>
  
</cfcomponent>

Java Application

The Java application to access the stock web service is built with Maven, and has the following POM file:

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

  <modelVersion>4.0.0</modelVersion>
  <groupId>org.adrianwalker.pocketpc</groupId>
  <artifactId>pocketpc-derby-ksoap2</artifactId>
  <packaging>jar</packaging>
  <version>0.1.0</version>
  <name>Pocket PC Derby kSOAP2</name>

  <description>
    Pocket PC Derby kSOAP2 example.
  </description>

  <repositories>
    <repository>
      <id>codehaus.org</id>
      <url>http://repository.codehaus.org</url>
    </repository>
    <!--
     ksoap2 isn't in any maven repoository, 
     so I'm distributing it with the project
    -->
    <repository>
      <id>project</id>
      <url>file://${basedir}/lib</url>
    </repository>
  </repositories>

  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.3</source>
          <target>1.3</target>
        </configuration>
      </plugin>
      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <executions>
          <execution>
            <id>fat</id>
            <phase>package</phase>
            <goals>
              <goal>single</goal>
            </goals>
            <configuration>
              <descriptors>
                <descriptor>src/main/assembly/fat.xml</descriptor>
              </descriptors>
            </configuration>
          </execution>
          <execution>
            <id>distribution</id>
            <phase>package</phase>
            <goals>
              <goal>single</goal>
            </goals>
            <configuration>
              <descriptors>
                <descriptor>src/main/assembly/distribution.xml</descriptor>
              </descriptors>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>

    <resources>
      <resource>
        <directory>src/main/resources</directory>
        <excludes>
          <exclude>**/*</exclude>
        </excludes>
      </resource>
    </resources>
  </build>

  <dependencies>
    <dependency>
      <groupId>org.apache.derby</groupId>
      <artifactId>derby</artifactId>
      <version>10.2.2.0</version>
    </dependency>

    <!--
    Included in project lib directory
    -->
    <dependency>
      <groupId>org.ksoap2</groupId>
      <artifactId>ksoap2-j2me-core</artifactId>
      <version>2.1.2</version>
    </dependency>
    
    <!--
      To emulate javax.microedition classes for testing,
      NOT to be deployed with the application
    -->
    <dependency>
      <groupId>org.microemu</groupId>
      <artifactId>microemulator</artifactId>
      <version>2.0.4</version>
    </dependency>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <reporting>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-pmd-plugin</artifactId>
      </plugin>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>findbugs-maven-plugin</artifactId>
        <configuration>
          <threshold>low</threshold>
          <effort>max</effort>
        </configuration>
      </plugin>
    </plugins>
  </reporting>
</project>

The build contains 2 Maven assembly descriptors. The first creates a fat jar which contains the project classes and all of required dependency classes. The second creates a directory distribution containing the fat jar and lnk file needed to run the application on the Pocket PC.

Pocket PC link files have a maximum command string length of 255 characters, listing seperate jars as java classpath arguments would exceed this length, hence the need for a single fat jar file.

I coudn't find kSOAP 2 in any maven repositories so I've included it with project source code in the /lib directory. In a real project you will probably want to install the jar into you local/company repository.

The microemulator dependency allows you to run the application from a desktop PC for testing, but is excluded from the distribution that will go on the Pocket PC.

The application itself is a simple AWT frame with a drop down list of stock names, a text field for the stock price and two buttons. The first button retrieves the stock price from the web service, and the second saves the value in the stock value field to the Derby database. When a stock name is selected, the application queries the database to see if it contains a row for that stock:

The main class, Stock.java, is an AWT frame which makes calls to a controller class.

Stock.java

package org.adrianwalker.pocketpc.stock;

import java.awt.event.WindowEvent;

public final class Stock extends java.awt.Frame {

  private static final String TITLE = "Pocket PC Example";
  private StockController stockController;

  public static void main(String args[]) {
    java.awt.EventQueue.invokeLater(new Runnable() {

      public void run() {
        Stock stock = new Stock();
        stock.setVisible(true);
      }
    });
  }

  public Stock() {
    setTitle(TITLE);

    initComponents();
    initListeners();

    stockNameChoice.add("IBM");
    stockNameChoice.add("MSFT");
    stockNameChoice.add("ADBE");
    stockNameChoice.add("ORCL");

    try {
      stockController = new StockController();
    } catch (Throwable t) {
      stockPriceField.setText("Error");
    }
  }

  // <editor-fold defaultstate="collapsed" desc="Generated Code">                          
  private void initComponents() {
    java.awt.GridBagConstraints gridBagConstraints;

    stockNameLabel = new java.awt.Label();
    stockNameChoice = new java.awt.Choice();
    stockPriceLabel = new java.awt.Label();
    stockPriceField = new java.awt.TextField();
    getStockPriceButton = new java.awt.Button();
    saveStockPriceButton = new java.awt.Button();

    addWindowListener(new java.awt.event.WindowAdapter() {
      public void windowClosing(java.awt.event.WindowEvent evt) {
        exitForm(evt);
      }
    });
    setLayout(new java.awt.GridBagLayout());

    stockNameLabel.setText("Stock Name:");
    gridBagConstraints = new java.awt.GridBagConstraints();
    gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
    gridBagConstraints.weightx = 1.0;
    gridBagConstraints.weighty = 1.0;
    add(stockNameLabel, gridBagConstraints);

    stockNameChoice.addItemListener(new java.awt.event.ItemListener() {
      public void itemStateChanged(java.awt.event.ItemEvent evt) {
        stockNameChoiceItemStateChanged(evt);
      }
    });
    gridBagConstraints = new java.awt.GridBagConstraints();
    gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
    gridBagConstraints.weightx = 1.0;
    gridBagConstraints.weighty = 1.0;
    add(stockNameChoice, gridBagConstraints);

    stockPriceLabel.setText("Stock Value:");
    gridBagConstraints = new java.awt.GridBagConstraints();
    gridBagConstraints.gridx = 0;
    gridBagConstraints.gridy = 1;
    gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
    gridBagConstraints.weightx = 1.0;
    gridBagConstraints.weighty = 1.0;
    add(stockPriceLabel, gridBagConstraints);
    gridBagConstraints = new java.awt.GridBagConstraints();
    gridBagConstraints.gridx = 1;
    gridBagConstraints.gridy = 1;
    gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
    gridBagConstraints.weightx = 1.0;
    gridBagConstraints.weighty = 1.0;
    add(stockPriceField, gridBagConstraints);

    getStockPriceButton.setLabel("Get Stock Value");
    getStockPriceButton.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        getStockPriceButtonActionPerformed(evt);
      }
    });
    gridBagConstraints = new java.awt.GridBagConstraints();
    gridBagConstraints.gridx = 0;
    gridBagConstraints.gridy = 2;
    gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
    gridBagConstraints.weightx = 1.0;
    gridBagConstraints.weighty = 1.0;
    add(getStockPriceButton, gridBagConstraints);

    saveStockPriceButton.setLabel("Save Stock Value");
    saveStockPriceButton.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        saveStockPriceButtonActionPerformed(evt);
      }
    });
    gridBagConstraints = new java.awt.GridBagConstraints();
    gridBagConstraints.gridx = 1;
    gridBagConstraints.gridy = 2;
    gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
    gridBagConstraints.weightx = 1.0;
    gridBagConstraints.weighty = 1.0;
    add(saveStockPriceButton, gridBagConstraints);

    pack();
  }// </editor-fold>                        

    private void exitForm(java.awt.event.WindowEvent evt) {                          
      try {
        stockController.close();
      } catch (Throwable t) {
        // ignore shutdown error
      } finally {
        dispose();
        System.exit(0);
      }
    }                         

    private void stockNameChoiceItemStateChanged(java.awt.event.ItemEvent evt) {                                                 
      String name = stockNameChoice.getItem(stockNameChoice.getSelectedIndex());
      double price;
      try {
        price = stockController.getDatbaseStockPrice(name);
      } catch (Throwable t) {
        stockPriceField.setText("Error");
        return;
      }

      if (price == -1) {
        stockPriceField.setText("Unknown");
      } else {
        stockPriceField.setText(String.valueOf(price));
      }
    }                                                

    private void getStockPriceButtonActionPerformed(java.awt.event.ActionEvent evt) {                                                    
      String name = stockNameChoice.getItem(stockNameChoice.getSelectedIndex());
      double price;
      try {
        price = stockController.getWebserviceStockPrice(name);
      } catch (Throwable t) {
        stockPriceField.setText("Error");
        return;
      }

      if (price == -1) {
        stockPriceField.setText("Unknown");
      } else {
        stockPriceField.setText(String.valueOf(price));
      }
    }                                                   

    private void saveStockPriceButtonActionPerformed(java.awt.event.ActionEvent evt) {                                                     

      String name = stockNameChoice.getItem(stockNameChoice.getSelectedIndex());
      double price;
      try {
        price = Double.parseDouble(stockPriceField.getText());
        stockController.saveStock(name, price);
      } catch (Throwable t) {
        stockPriceField.setText("Error");
        return;
      }
    }                                                    

  // Variables declaration - do not modify                     
  private java.awt.Button getStockPriceButton;
  private java.awt.Button saveStockPriceButton;
  private java.awt.Choice stockNameChoice;
  private java.awt.Label stockNameLabel;
  private java.awt.TextField stockPriceField;
  private java.awt.Label stockPriceLabel;
  // End of variables declaration                   

  private void initListeners() {
    addWindowListener(new java.awt.event.WindowAdapter() {

      public void windowIconified(WindowEvent evt) {
        exitForm(evt);
      }
    });
  }
}

Two useful things to note about this code are:

  1. When you close a Java frame on the Pocket PC the program does not fire the window closing event, but instead is actually minimised. To exit the application you must listen for the window iconified event and use that to exit the application.
  2. You must call dispose() on the frame when exiting, otherwise the memory the application was using it not released, even when System.exit(0) is called.

StockController.java contains some Derby configuration settings and the web service end point URL which you will probaly want to change before deploying to the Pocket PC. The actual database and web service logic is delegated to 2 other classes, DatabaseController.java and WebServiceController.java.

StockController.java

package org.adrianwalker.pocketpc.stock;

import org.adrianwalker.pocketpc.stock.database.DatabaseController;
import org.adrianwalker.pocketpc.stock.webservice.WebServiceController;

public final class StockController {

  private static final String DATABASE_NAME = "\\StockDB";
  private static final String ENDPOINT = "http://localhost:8500/pocketpc-web-service/Stock.cfc";
  private static final String NAMESPACE = "http://www.example.org/stock";

  // default 1000, minimum 40
  private static final String PAGE_CACHE_SIZE = "40";
  // values: 4096 (default), 8192, 16384, or 32768
  private static final String PAGE_SIZE = "4096";
  private static final String ROW_LOCKING = "false";
  private final DatabaseController databaseController;
  private final WebServiceController webServiceController;

  public StockController() throws Exception {

    System.setProperty("derby.storage.pageCacheSize", PAGE_CACHE_SIZE);
    System.setProperty("derby.storage.pageSize", PAGE_SIZE);
    System.setProperty("derby.storage.rowLocking", ROW_LOCKING);

    databaseController = new DatabaseController(DATABASE_NAME);
    webServiceController = new WebServiceController(ENDPOINT, NAMESPACE);
  }

  public void close() throws Exception {
    try {
      webServiceController.close();
    } finally {
      try {
        databaseController.close();
      } catch (Exception e) {
        // ignore shutdown error
      }
    }
  }

  public double getWebserviceStockPrice(final String name) throws Exception {
    return webServiceController.getStockPrice(name);
  }

  public double getDatbaseStockPrice(final String name) throws Exception {
    return databaseController.readStockPrice(name);
  }

  public void saveStock(final String name, final double price) throws Exception {

    if (price < 0) {
      throw new IllegalArgumentException("Price must be positive");
    }

    if (databaseController.readStockPrice(name) == -1) {
      databaseController.createStock(name, price);
    } else {
      databaseController.updateStock(name, price);
    }
  }
}

WebServiceController.java

package org.adrianwalker.pocketpc.stock.webservice;

import org.ksoap2.serialization.SoapObject;
import org.ksoap2.serialization.SoapPrimitive;
import org.ksoap2.serialization.SoapSerializationEnvelope;
import org.ksoap2.transport.HttpTransport;

public final class WebServiceController {

  private static final String STOCK_PRICE_METHOD = "getStockPrice";
  private final String endpoint;
  private final String namespace;
  private final HttpTransport httpTransport;

  public WebServiceController(final String endpoint, final String namespace) {
    this.endpoint = endpoint;
    this.namespace = namespace;
    this.httpTransport = new HttpTransport(this.endpoint);
  }

  public double getStockPrice(final String stockName) throws Exception {
    SoapSerializationEnvelope envelope = new SoapSerializationEnvelope(SoapSerializationEnvelope.VER11);
    SoapObject request = new SoapObject(this.namespace, STOCK_PRICE_METHOD);
    envelope.setOutputSoapObject(request);
    request.addProperty("stockName", new SoapPrimitive(this.namespace, "string", stockName));
    httpTransport.call("", envelope);
    SoapObject body = (SoapObject) envelope.bodyIn;

    SoapPrimitive stockPrice = (SoapPrimitive) body.getProperty("getStockPriceReturn");

    return Double.parseDouble(stockPrice.toString());
  }

  public void close() {
    httpTransport.reset();
  }
}

DatabaseController.java

package org.adrianwalker.pocketpc.stock.database;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import org.apache.derby.jdbc.EmbeddedSimpleDataSource;

public final class DatabaseController {

  private static final String CREATE_DATABASE = "create";
  private static final String DATABASE_SHUTDOWN = "shutdown";
  private static final String CREATE_TABLE =
          "CREATE TABLE APP.STOCK ( " +
          "ID    BIGINT NOT NULL GENERATED ALWAYS AS IDENTITY PRIMARY KEY," +
          "NAME  VARCHAR(10) NOT NULL UNIQUE," +
          "PRICE DOUBLE NOT NULL" +
          ")";
  private static final String INSERT_STOCK =
          "INSERT INTO APP.STOCK(NAME, PRICE) " +
          "VALUES(?,?)";
  private static final String SELECT_STOCK_PRICE =
          "SELECT PRICE " +
          "FROM APP.STOCK " +
          "WHERE NAME = ?";
  private static final String UPDATE_STOCK_PRICE =
          "UPDATE APP.STOCK " +
          "SET PRICE = ? " +
          "WHERE NAME = ?";
  private static final String DROP_TABLE =
          "DROP TABLE APP.STOCK";
  private EmbeddedSimpleDataSource dataSource;
  private Connection connection;
  private PreparedStatement insertStock;
  private PreparedStatement selectStockPrice;
  private PreparedStatement updateStockPrice;

  public DatabaseController(final String databaseName) throws SQLException {
    createDataSource(databaseName);
    createConnection();

    if (!tableExists()) {
      createTable();
    }

    createPreparedStatements();
  }

  private void createDataSource(final String databaseName) {
    this.dataSource = new EmbeddedSimpleDataSource();
    this.dataSource.setDatabaseName(databaseName);
    this.dataSource.setCreateDatabase(CREATE_DATABASE);
  }

  private void createConnection() throws SQLException {
    this.connection = dataSource.getConnection();
  }

  private void createPreparedStatements() throws SQLException {
    this.insertStock = connection.prepareStatement(INSERT_STOCK);
    this.selectStockPrice = connection.prepareStatement(SELECT_STOCK_PRICE);
    this.updateStockPrice = connection.prepareStatement(UPDATE_STOCK_PRICE);
  }

  private boolean tableExists() throws SQLException {
    ResultSet tables = connection.getMetaData().getTables(null, "APP", "STOCK", new String[]{"TABLE"});
    return tables.next();
  }

  public void createTable() throws SQLException {
    Statement statement = connection.createStatement();
    try {
      statement.execute(CREATE_TABLE);
    } catch (SQLException se) {
      se.printStackTrace();
    } finally {
      statement.close();
    }
  }

  public void dropTable() throws SQLException {
    Statement statement = connection.createStatement();
    try {
      statement.execute(DROP_TABLE);
    } catch (SQLException se) {
      se.printStackTrace();
    } finally {
      statement.close();
    }
  }

  public double readStockPrice(final String name) throws SQLException {
    this.selectStockPrice.setString(1, name);

    double price = -1;
    ResultSet resultSet = selectStockPrice.executeQuery();
    try {
      while (resultSet.next()) {
        price = resultSet.getDouble(1);
        return price;
      }
    } finally {
      resultSet.close();
    }

    return price;
  }

  public void createStock(final String name, final double price) throws SQLException {
    this.insertStock.setString(1, name);
    this.insertStock.setDouble(2, price);
    this.insertStock.execute();
  }

  public void updateStock(final String name, final double price) throws SQLException {
    this.updateStockPrice.setDouble(1, price);
    this.updateStockPrice.setString(2, name);
    this.updateStockPrice.execute();
  }

  public void close() throws SQLException {
    try {
      this.insertStock.close();
      this.selectStockPrice.close();
      this.updateStockPrice.close();
    } finally {
      try {
        this.connection.close();
      } finally {
        this.dataSource.setShutdownDatabase(DATABASE_SHUTDOWN);
        this.dataSource.getConnection();
      }
    }
  }
}

Build & Deploy

Build the project like an regular Maven build, 'mvn clean install', making sure your web service is available to pass all the unit tests.

The build should create a directory distribution in:

target\pocketpc-derby-ksoap2-0.1.0-distribution.dir

Copy the contents of this directory to the Pocket PC via ActiveSync to the directory:

\Program Files\Java\

Once the files have copied you can run the stock link file to run the app:

\Program Files\Java\pocketpc-derby-ksoap2-0.1.0\stock.lnk

Source Code