Showing posts with label JavaME. Show all posts
Showing posts with label JavaME. Show all posts

Friday, 22 August 2008

Consuming Web Services from Android

Earlier this week, Google released Android 0.9 SDK Beta. As usual, I couldn't wait to try it out.

Unlike JavaME (especially MIDP 1.0), Android is targetting mobile devices with more grunt - it's bundled with many open source libraries, such as Apache Harmony (open source Java SE), Unicode library, JUnit, Apache Commons, ASN.1 libarary and more from Bouncy Castle, kXML, etc. The android.jar file is 11MB!

It is time to write a web service consumer on Android, to consume my WCF web service as well as RESTful service that I developed for a demo.

The Android does not contain any tools to help building SOAP based web service clients. Google is a proponent of REST services. It is no surprise that the SDK is not bundled with any SOAP-related tools. An alternative is to add kSOAP 2 to my test project. But I quickly dismissed the idea as my web service built in WCF is not JSR-172 compliant.

Like my exercise in JavaME, I decided to call the RESTful version of the same service built in NetBeans 6.1 instead.

I created a new Android project - SvdemoAndroid, using Eclipse 3.4 with the Android plugin installed. Because my Android Activity will be accessing the internet for the HTTP connection (although it's hosted on my machine), I had to add the line <uses-permission android:name="android.permission.INTERNET" /> to my AndroidManifest.xml file. Now the file looks something like:



    <uses-permission android:name="android.permission.INTERNET" /> 
    
        
            
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            
        
    
 

Android is bundled with Apache HttpClient 4. So invoking a REST service is simply a matter of openning up a HTTP link using the HttpClient and process the response. The following two methods show the consumption of two different RESTful services. The first method getKeywords() calls a service returning a plain text on HTTP.

        static String SERVER_HOST="192.168.2.100";
 static int SERVER_PORT = 8080;
 static String URL1 = "/SvdemoRestful/resources/mySpace?url=http://wpickup02/MySpace_Com_John.htm";
 static String URL2 = "/SvdemoRestful/resources/promo";
 /**
     * Call the RESTful service to get a list of keywords from the web page.
     * @param target - the target HTTP host for the RESTful service.
     * @return - comma delimited keywords. May contain spaces. If no keywords found, return null.
     */
    private String getKeywords(HttpHost target) {
        String keywords=null;
     HttpEntity entity = null;
     HttpClient client = new DefaultHttpClient();
     HttpGet get = new HttpGet(URL1);
     try {
   HttpResponse response=client.execute(target, get);
   entity = response.getEntity();
   keywords = EntityUtils.toString(entity);
  } catch (Exception e) {
   e.printStackTrace();
  } finally {
   if (entity!=null)
    try {
     entity.consumeContent();
    } catch (IOException e) {}
  }
  return keywords;
    }

The second method calls another service (on URL2). This service returns a XML string containing the details of a Promotion. Unlike JavaME, Android SDK is bundled with DOM parser. Since my XML string is pretty short I decided to use DOM instead of kXML's pull parser. Note that PromoInfo is just a data object holding all the promotion information in its fields.

/**
     * Call the REST service to retrieve the first matching promotion based
     * on the give keywords. If none found, return null.
     * @param target - the target HTTP host for the REST service.
     * @param keywords - comma delimited keywords. May contain spaces.
     * @return - PromoInfo that matches the keywords. If error or no match, return null.
     */
    private PromoInfo searchPromo(HttpHost target, String keywords) {
     if(keywords==null)
      return null;
     
     PromoInfo promo=null;
     Document doc = null;
     HttpClient client = new DefaultHttpClient();
     HttpGet get = new HttpGet(URL2+"/"+keywords.replaceAll(" ", "%20"));
     try {
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
   DocumentBuilder builder = factory.newDocumentBuilder();
   HttpEntity entity = client.execute(target, get).getEntity();
   
   doc = builder.parse(entity.getContent());
   NodeList promotions = doc.getElementsByTagName("Promotion");
   
   if(promotions!=null) {
    Node promoNode = promotions.item(0);
    
    if (promoNode!=null) {
     promo=new PromoInfo();
     NodeList nodeList = promoNode.getChildNodes();
     int len = nodeList.getLength();
     for(int i=0; i<len; i++) {
      Node node = nodeList.item(i);
      String value = this.getNodeValue(node);
      if("Name".equals(node.getNodeName())) {
       promo.setName(value);
      } else if("Description".equals(node.getNodeName())) {
       promo.setDescription(value);
      } else if("Venue".equals(node.getNodeName())) {
       promo.setVenue(value);
      } else if("Name".equals(node.getNodeName())) {
       promo.setName(value);
      } else if("DateTime".equals(node.getNodeName())) {
       promo.setDateTime(value);
      }
     }
    }
   }
  } catch (Exception e) {
   e.printStackTrace();
   promo=null;
  } finally {
   
  }
  return promo;
    }
    private String getNodeValue(Node node) {
     NodeList children = node.getChildNodes();
     if(children.getLength()>0) {
      return children.item(0).getNodeValue();
     } else
      return null;
    }

Now that I have invoked the web services and got the data back to my Android phone, it is time to display them on screen. Android adopts a similar approach to Microsoft WPF, where you can define the view layout and styles in a XML file, so that the Java code is freed up from screen rendering code and can focus more on the business logic.

I created a style.xml file under the {project}/res/values directory to define the styles for my screen widgets.



    <style name="LabelText">
        18sp
        #fff
        fill_parent 
        wrap_content 
        5px
    </style>
    <style name="ContentText">
        14sp
        #000
        #e81
        true
        
        fill_parent 
        wrap_content 
    </style>

Then my view layout XML can use the styles. Here is the {project}/res/layout/main.xml file:



    <TextView style="@style/LabelText" android:text="Name:"/>
    <TextView style="@style/ContentText" android:id="@+id/tvName" />
    <TextView style="@style/LabelText" android:text="Description:"/>
    <TextView style="@style/ContentText" android:id="@+id/tvDescription" />
    <TextView style="@style/LabelText" android:text="Venue:"/>
    <TextView style="@style/ContentText" android:id="@+id/tvVenue" />
    <TextView style="@style/LabelText" android:text="Date:"/>
    <TextView style="@style/ContentText" android:id="@+id/tvDate" />

Note that by adding the android:text="@+id/tvName", the Android Eclipse Plugin will automatically regenerate the R.java file to add fields into the id class, so that you can reference the widget from Java code using findViewById(R.id.tvName). Here is the code for the onCreate() method:

/** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        HttpHost target = new HttpHost(SERVER_HOST, SERVER_PORT, "http");
        String keywords = getKeywords(target);
        
        setContentView(R.layout.main);
        if(keywords!=null) {
         PromoInfo promo = searchPromo(target, keywords);
         if(promo!=null) {
          ((TextView)this.findViewById(R.id.tvName)).setText(promo.getName());
          ((TextView)this.findViewById(R.id.tvDescription)).setText(promo.getDescription());
          ((TextView)this.findViewById(R.id.tvDate)).setText(promo.getDateTime());
          ((TextView)this.findViewById(R.id.tvVenue)).setText(promo.getVenue());
         }
        }
    }

Running the application:

Android screenshot

Related Posts:

Tuesday, 19 August 2008

Consuming Web Services By JavaME

The J2ME was designed for very resource-constrained hardware. As the devices get more and more powerful (N95, iPhone, HTC Android phones), the JavaME's Mobile Information Device Profiles (MIDP) desperately need an upgrade. Before JSR-271 (MIDP 3.0) is finalised Sun has recognised the poor UI capability of JavaME and released the Light Weight UI Toolkit (LWUIT) to stay competitive. Another good 3rd-party framework is J2ME Polish.

Here I attempt to call the same web services that I developed as part of a demo using JavaME and then display the results using LWUIT.

I first tried to consume the web services developed using WCF. The Sun's Wireless Toolkit (WTK2.5.2) comes with a utility to generate SOAP client proxy from WSDL. (An alternative is to use KSOAP2). However, JavaME only supports a subset of JAXP and JAX-RPC as defined in JSR-172. My web service data contract contains a DateTime field, which violates JSR-172. Also it is mapped to java.util.Calendar, which is not supported by JavaME. So I could not call the SOAP web service as it is from JavaME. I guess I will have to wait for MIDP 3.0 or even later. In any case, full SOAP web services are too much of a heavy weight for mobile devices. It is better to use RESTful services.

Fortunately, I have also developed a RESTful version of the services using NetBeans 6.1 under the project SvdemoRestful. So I created a MIDP 2.0 project named SvdemoWUIT in NetBeans and included the LWUIT JAR in the project.

There are two quick and easy ways to consume RESTful services:

The first way is to use one of NetBeans MIDP wizards to create "Mobile Client to Web Application". This wizard will take a Web Application (or a Web App project in NB) to discover the RESTful services and generates a pair of servlet (in the web app project) and mobile client (in the MIDP project) code to wrap the service. The screenshot shows the generated files highlighted in red. Using the generated mobile client in the MIDlet:

// create an instance of the mobile client proxy
WebToMobileClient client=new WebToMobileClient();

// consume the web service via the client
String keywords = client.getText("http://localhost/MySpace_Com_John.htm");
System.out.println("keywords:"+keywords);
String promoXml = client.getXml(keywords);
System.out.println("promo:"+promoXml);           
The corresponding stdout shows:
keywords:folk songs, pop music, chinese music ,battle, action, comedy
promo:801Batman The Dark Knight
                        Meet stars in Batman in person - Chritian Bale, Michael Caine.
                Star City2008-7-30 10:00:00Batman, action

There seemed to be a bug in the wizard: it only generated methods from one web service resource of the SvdemoRestful project although I specified both resources. Therefore, I had to modify the Utility.java and WebToMobileClient.java by hand to add the missing methods.

The second way is to use the javax.microedition.io.HttpConnection and deal with the low level stuff myself. I needed to parse the Promotion XML and to populate the PromoInfo object. JavaME only offers SAX for XML parsing, so I had to get the InputStream from the HttpConnection and pass it to the SAXParser. SAX uses callback mechanism. Therefore, a XmlHandler class had to be defined.

The MIDlet code snippet:

public class SvdemoWUITMidlet extends MIDlet implements ActionListener {
    static String promoUrl="http://localhost:8080/SvdemoRestful/resources/promo";
    // data object to hold the XML contents in its corresponding fields
    PromoInfo promo=null;
    public PromoInfo getPromo() {
        return this.promo;
    }
    public void setPromo(PromoInfo promo) {
        this.promo=promo;
    }
    public void startApp() {
        Display.init(this);
        HttpConnection hc=null;

        WebToMobileClient client=new WebToMobileClient();
        try {
            String keywords = client.getText("http://localhost/MySpace_Com_John.htm");
            System.out.println("keywords:"+keywords);
            String promoXml = client.getXml(keywords);
            System.out.println("promo:"+promoXml);
           
            hc=(HttpConnection)Connector.open(promoUrl + StringUtil.replace(keywords," ", "%20"));
            parse(hc.openInputStream());

            initialiseForm();
        } catch (IOException ex) {
            ex.printStackTrace();
        } finally {
            try { if (hc!=null) hc.close(); }
            catch(IOException ignored) {}
        }
    }

    private void parse(InputStream is) {
        SAXParserFactory factory = SAXParserFactory.newInstance();
        try {
            SAXParser saxParser = factory.newSAXParser();
            InputSource inputSource = new InputSource(is);
            saxParser.parse(is,new XmlHandler(this));

        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
...

The XmlHandler.java file:

/*
 * To change this template, choose Tools  Templates
 * and open the template in the editor.
 */

package svdemo;

import java.util.Stack;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 *
 * @author ROMENL
 */
public class XmlHandler extends DefaultHandler {
    static String TAG_PROMOTION="Promotion";
    static String TAG_NAME="Name";
    static String TAG_DESCRIPTION="Description";
    static String TAG_VENUE="Venue";
    static String TAG_DATETIME="DateTime";
   
    private SvdemoWUITMidlet midlet;
    private PromoInfo promo=null;
    private Stack tagStack=new Stack();
    public XmlHandler(SvdemoWUITMidlet midlet) {
        this.midlet=midlet;
    }

    public void startDocument() throws SAXException {}
    public void startElement(String uri, String localName, String qName,
    Attributes attributes) throws SAXException {
        if(TAG_PROMOTION.equals(qName)) {
            promo=new PromoInfo();
        }
        tagStack.push(qName);
    }
    public void characters(char[] ch, int start, int length)
            throws SAXException  {
        String chars = new String(ch, start, length).trim();

        if(chars.length() > 0) {
            String qName = (String)tagStack.peek();
     
            if (TAG_NAME.equals(qName)) {
                promo.setName(chars);
            } else if(TAG_DESCRIPTION.equals(qName)){
                promo.setDescription(chars);
            } else if(TAG_VENUE.equals(qName)) {
                promo.setVenue(chars);
            } else if(TAG_DATETIME.equals(qName)) {
                promo.setDateTime(chars);
            }
        }
    }

    public void endElement(String uri, String localName, String qName,
    Attributes attributes) throws SAXException  {
        tagStack.pop();
    }

    public void endDocument() throws SAXException {
        midlet.setPromo(promo);
    }
}

Finally, presenting the retrieved data on the mobile device - the initialiseForm() method in the MIDlet:

    private void initialiseForm() {
        Form f = new Form("JME LWUIT WS Demo!");
        
        f.setTransitionInAnimator(Transition3D.createCube(2000, false));
        f.setTransitionOutAnimator(Transition3D.createCube(2000, true));
        f.getStyle().setBgColor(0x0000ff);
        
        if(promo!=null) {
            f.addComponent(createLabel("Name:"));
            f.addComponent(createTextArea(1, promo.getName()));

            f.addComponent(createLabel("Description:"));
            f.addComponent(createTextArea(2, promo.getDescription()));

            f.addComponent(createLabel("Venue:"));
            f.addComponent(createTextArea(1, promo.getVenue()));

            f.addComponent(createLabel("Date:"));
            f.addComponent(createTextArea(1, promo.getDateTime()));
        }
        f.show();
      
        Command exitCommand = new Command("Exit");
        f.addCommand(exitCommand);
        f.setCommandListener(this);
    }
    
    private Label createLabel(String labelText) {
        Label label=new Label(labelText);
        Font font=Font.createSystemFont(Font.FACE_SYSTEM, 
                Font.STYLE_BOLD, Font.SIZE_MEDIUM);
        label.getStyle().setFont(font);
        return label;
    }
    
    private TextArea createTextArea(int rows, String text) {
        TextArea ta=new TextArea(rows ,20, TextArea.ANY);
        ta.setEditable(false);
        ta.setText(text);
        return ta;
    }

The following screenshots show the LWUIT 3D cube transition effect and the MIDlet output form respectively.

Related Posts: