Showing posts with label Web Service. Show all posts
Showing posts with label Web Service. Show all posts

Monday, 22 June 2009

JAXB Custom Data Binding

In a previous post I experimented consuming WCF web services using various Java WS frameworks and tools. As pointed out by Alex, the one that I missed out was wsimport which is bundled as part of JSE6.

Like many other tools, it supports both Ant task and command line interface (CLI). The CLI for wsimport is quite simple - in my case I generated the source code and client stub library like so:

D:\Program Files\Java\jdk1.6.0_11\bin>wsimport -d /temp/generated -s /temp/gensrc -keep http://localhost/PromoService.svc?wsdl
parsing WSDL...


generating code...

D:\Program Files\Java\jdk1.6.0_11\bin>
All the rest is similar to the results of IntelliJ shown in my previous post. There are two problems with the generated PromoInfo.java which is a data/value object:
  1. string fields are generated as JAXBElement<String>
  2. dateTime fields are generated as XMLGregorianCalendar
I want to use core java data types on the data objects so that they can be easily integrated with other frameworks without having to do conversion. Examining my schema (on http://localhost/PromoService.svc?xsd=xsd2) the PromoInfo complex type is defined as


  
    
      
      
      
      
    
  


It is obvious that the xs:string and xs:dateTime were not converted into the desired java types. To solve my problems I specified customised JAXB binding rules in an external file - custombinding.xml like so

 
     
 

The attribute generateElementProperty="false" on line 2 tells wsimport not to generate JAXBElement but to generate native java data types instead.

The javaType element on line 3 defines the binding between "xs:dateTime" and "java.util.Date" because by default xml dateTime binds to javax.xml.datatype.XMLGregorianCalendar as shown here.

Once the binding is defined, rerunning the wsimport tool with the -b switch will produce the desired output:
D:\Program Files\Java\jdk1.6.0_11\bin>wsimport -d /temp/generated -s /temp/gensrc -b /temp/custombinding.xml http://localhost/PromoService.svc?wsdl
parsing WSDL...


generating code...
Note: D:\temp\gensrc\org\w3\_2001\xmlschema\Adapter1.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.

This time the generated PromoInfo.java looks much better:
...
public class PromoInfo {

    @XmlElement(name = "PromoDateTime", type = String.class)
    @XmlJavaTypeAdapter(Adapter1 .class)
    @XmlSchemaType(name = "dateTime")
    protected Date promoDateTime;
    @XmlElement(name = "PromoDescription", nillable = true)
    protected String promoDescription;
    @XmlElement(name = "PromoName", nillable = true)
    protected String promoName;
    @XmlElement(name = "PromoVenue", nillable = true)
    protected String promoVenue;
...


Sunday, 5 October 2008

Consuming RESTful Service with Erlang

Having been working in the telco industry for over 10 years, I can't help feeling a bit ashamed for not having learned Erlang. Erlang was developed by Ericsson - the leading Swedish-based network equipment provider, and has been used in many of the network switching equipment produced by Ericsson. Erlang has gained more traction recently, especially in the last year or two.

Here, I write an Erlang program to consume a demo RESTful service that I developed a couple of months ago. The Erlang code is based on example by Dave Thomas - the author of Programming Ruby. The detail of the RESTful service is available in my previous post.

There are two services that I want to consume.

The first one accepts a social web site URL and scrapes the page for the person's interests in movies and music to return a list of comma-delimited keywords. For example,

http://localhost:8080/SvdemoRestful/resources/webPageKeywords?url=http://localhost/someWebPage.html

if any keywords are found, then they are returned in the body of the HTTP response as a string; otherwise, the HTTP body is empty. For example,

folk songs, pop music, chinese music ,battle, action, comedy

Notice the spaces in the above string. The second service I want to consume accepts the above string to search for any matching promotion in a mock up XML database. If any is found then the XML string will be returned in the HTTP body; otherwise, the string <Empty/> is returned in the HTTP body. For example, the following URL will return the promotion information in XML below:

http://localhost:8080/SvdemoRestful/resources/promo/jazz

the returned XML:

<Promotion>
  <Code>802</Code> 
  <Name>Jazz Night</Name> 
  <Description>Jazz lovers' do not miss this once in a lifetime opportunity.</Description> 
  <Venue>The Jazz Club</Venue> 
  <DateTime>2008-10-30 21:00:00</DateTime> 
  <Tags>Jazz</Tags> 
</Promotion>

Now, let's do this in Erlang.

Create a file called svdemoClient.erl. The following code will consume the first RESTful service:

-module(svdemoClient).
-export([get_keywords/1]).

-define(BASE_URL, "http://localhost:8080/SvdemoRestful/resources").
-define(PROMO_URL, ?BASE_URL ++ "/promo/").
-define(KEYWORDS_URL, ?BASE_URL "/webPageKeywords"). % also works without ++

keywords_url_for(Url) -> ?KEYWORDS_URL ++ "?url=" ++ Url.
get_keywords(Url) ->
 URL = keywords_url_for(Url),
 { ok, {_Status, _Header, Body} } = http:request(URL),
 Body.

In Erlang, function names and Atoms must start with lower-case letters; variable names must start with upper-case letters or underscore (meaning the variable is not used/read).

The -define() macro in Erlang is similar to #define in C/C++. In the above example, after defining BASE_URL, any occurance of ?BASE_URL will be replaced with the string "http://localhost:8080/SvdemoRestful/resources".

The get_keywords() function returns the body of the HTTP response from requesting the given Url. The Body is either a comma-delimited string, or an empty collection. Executing the above code in Erlang:

127> c("d:/projects/svdemoErl/svdemoClient.erl").                            
{ok,svdemoClient}
128> svdemoClient:get_keywords("http://localhost/myWebPage.htm").     
"folk songs, pop music, chinese music ,battle, action, comedy "
129> svdemoClient:get_keywords("http://localhost/someOtherPage.htm").
[]
130> 

To consume the second RESTful service, the search_promo() function is added.

promo_url_for(Keywords) -> ?PROMO_URL ++ utils:url_encode(Keywords).
search_promo(Keywords) ->
 URL = promo_url_for(Keywords), 
 { ok, {_Status, _Header, Body} } = http:request(URL),
 
 %%% Now that the XML is in the Body variable, let's parse it.
 if
  Body == "<Empty/>" ->
   not_found;
  true ->
   {ParseResult, _Misc} = xmerl_scan:string(Body),
   [ #xmlText{value=Code} ] = xmerl_xpath:string("//Code/text()", ParseResult),
   [ #xmlText{value=Name} ] = xmerl_xpath:string("//Name/text()", ParseResult),
   [ #xmlText{value=Description} ] = xmerl_xpath:string("//Description/text()", ParseResult),
   [ #xmlText{value=Venue} ] = xmerl_xpath:string("//Venue/text()", ParseResult),
   [ #xmlText{value=DateTime} ] = xmerl_xpath:string("//DateTime/text()", ParseResult),
   { Code, Name, Description, Venue, DateTime }
 end.

Erlang/OTP download comes with XML parser and XPath support in the xmerl application, which is not part of the Erlang standard library (stdlib). To use the XML functions, the header file must be included:

-include_lib("xmerl/include/xmerl.hrl").

Note that the keywords contain spaces, which must be URL-encoded before passing to Erlang's http:request() function. I stole the url_encode() function from YAWS and put it in utils.erl file.

To string the two service consumptions together:

search_promo_from_url(Url) ->
 Keywords=get_keywords(Url),
 if
  Keywords == [] ->
   not_found;
  true ->
   search_promo(Keywords)
 end.

Calling the function in Erlang shell:

126> svdemoClient:search_promo_from_url("http://localhost/MyWebPage.htm")
. 
{"801",
 "Batman The Dark Knight",
 "\n\t\t\tMeet stars in Batman in person - Chritian Bale, Michael Caine.\n\t\t",

 "Star City",
 "2008-7-30 10:00:00"}

The final svdemoClient.erl file:

-module(svdemoClient).
-export([get_keywords/1, search_promo/1, search_promo_from_url/1]).
-include_lib("xmerl/include/xmerl.hrl").

-define(BASE_URL, "http://localhost:8080/SvdemoRestful/resources").
-define(PROMO_URL, ?BASE_URL ++ "/promo/").
-define(KEYWORDS_URL, ?BASE_URL "/webPageKeywords"). % also works without ++

keywords_url_for(Url) -> ?KEYWORDS_URL ++ "?url=" ++ Url.
get_keywords(Url) ->
 URL = keywords_url_for(Url),
 { ok, {_Status, _Header, Body} } = http:request(URL),
 Body.

promo_url_for(Keywords) -> ?PROMO_URL ++ utils:url_encode(Keywords).
search_promo(Keywords) ->
 URL = promo_url_for(Keywords), 
 { ok, {_Status, _Header, Body} } = http:request(URL),
 
 %%% Now that the XML is in the Body variable, let's parse it.
 if
  Body == "<Empty/>" ->
   not_found;
  true ->
   {ParseResult, _Misc} = xmerl_scan:string(Body),
   [ #xmlText{value=Code} ] = xmerl_xpath:string("//Code/text()", ParseResult),
   [ #xmlText{value=Name} ] = xmerl_xpath:string("//Name/text()", ParseResult),
   [ #xmlText{value=Description} ] = xmerl_xpath:string("//Description/text()", ParseResult),
   [ #xmlText{value=Venue} ] = xmerl_xpath:string("//Venue/text()", ParseResult),
   [ #xmlText{value=DateTime} ] = xmerl_xpath:string("//DateTime/text()", ParseResult),
   { Code, Name, Description, Venue, DateTime }
 end.
 
search_promo_from_url(Url) ->
 Keywords=get_keywords(Url),
 if
  Keywords == [] ->
   not_found;
  true ->
   search_promo(Keywords)
 end.

The utils.erl file (copied from YAWS):

-module(utils).
-export([integer_to_hex/1, url_encode/1]).

integer_to_hex(I) ->
     case catch erlang:integer_to_list(I, 16) of
         {'EXIT', _} ->
             old_integer_to_hex(I);
         Int ->
             Int
     end.
 
 
old_integer_to_hex(I) when I<10 ->
     integer_to_list(I);
old_integer_to_hex(I) when I<16 ->
     [I-10+$A];
old_integer_to_hex(I) when I>=16 ->
     N = trunc(I/16),
     old_integer_to_hex(N) ++ old_integer_to_hex(I rem 16).
 

url_encode([H|T]) ->
     if
         H >= $a, $z >= H ->
             [H|url_encode(T)];
         H >= $A, $Z >= H ->
             [H|url_encode(T)];
         H >= $0, $9 >= H ->
             [H|url_encode(T)];
         H == $_; H == $.; H == $-; H == $/; H == $: -> % FIXME: more..
             [H|url_encode(T)];
         true ->
             case integer_to_hex(H) of
                 [X, Y] ->
                     [$%, X, Y | url_encode(T)];
                 [X] ->
                     [$%, $0, X | url_encode(T)]
             end
     end;
 
url_encode([]) ->
     [].

Related 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:

Monday, 18 August 2008

Consuming WCF Web Service Using Groovy Client

Last month, I developed some simple web services using WCF and consumed it using various web service frameworks in Java. Now I have implemented the web service client using GroovyWS, which is an add-on module to Groovy. It uses CXF under the hood.

To add the GroovyWS library, I simply copied its JAR file groovy-all-1.5.6.jar into the Groovy's lib directory (it can either go in {groovy.home}/lib or {user.home}/.groovy/lib according to the groovy-starter.conf file). Then run the following script from Groovy Console:

import groovyx.net.ws.WSClient
def proxy = new WSClient("http://localhost/PromoService.svc?wsdl", 
                this.class.classLoader)
        
def keywords = proxy.GetKeywordsFromMySpace("http://someurl/somepage.htm")
if(keywords!=null) {
    if (proxy.HasPromo(keywords)) {
        def promo = proxy.GetFirstPromo(keywords)
        println "Promo: ${promo?.promoName?.getValue()}, ${promo?.promoDateTime}"
    }
}
gives the result:
BindingInfo = [BindingInfo http://schemas.xmlsoap.org/wsdl/soap/]
o = SOAPBinding ({http://schemas.xmlsoap.org/wsdl/soap/}binding):
required=null
transportURI=http://schemas.xmlsoap.org/soap/http
Promo: Batman The Dark Knight, 2008-07-30T10:00:00

That's all!

I then re-did it using Eclipse 3.4 with Groovy Plug-in to create the Groovy project by following the instructions from the same web site. Then I added the JAR files into the dependency:

  • ant.jar from Apache Ant 1.7.0 - this is needed by GroovyWS but not included in its distribution
  • groovy-all-1.5.6.jar
The Groovy code for the test client is very simple:
package svdemo

import groovyx.net.ws.WSClient

/**
 * @author ROMENL
 *
 */
public class Program{

 /**
  * @param args
  */
 public static void main(def args){
  def proxy = new WSClient("http://localhost/PromoService.svc?wsdl", 
    Program.class.classLoader)
  
  def keywords = proxy.GetKeywordsFromMySpace("http://someurl/somepage.htm")
  if(keywords!=null) {
   if (proxy.HasPromo(keywords)) {
    def promo = proxy.GetFirstPromo(keywords)
    println "Promo: ${promo?.promoName?.getValue()}, ${promo?.promoDateTime}"
   }
  }
 }
}
Compiling then running the program yielded:
18/08/2008 12:19:33 PM org.apache.cxf.endpoint.dynamic.DynamicClientFactory outputDebug
INFO: Created classes: org.tempuri.GetFirstPromo, org.tempuri.GetFirstPromoResponse, org.tempuri.GetKeywordsFromMySpace, org.tempuri.GetKeywordsFromMySpaceResponse, org.tempuri.HasPromo, org.tempuri.HasPromoResponse, org.tempuri.ObjectFactory, com.microsoft.schemas._2003._10.serialization.ObjectFactory, org.datacontract.schemas._2004._07.svdemo.ObjectFactory, org.datacontract.schemas._2004._07.svdemo.PromoInfo
BindingInfo = [BindingInfo http://schemas.xmlsoap.org/wsdl/soap/]
o = SOAPBinding ({http://schemas.xmlsoap.org/wsdl/soap/}binding):
required=null
transportURI=http://schemas.xmlsoap.org/soap/http
Promo: Batman The Dark Knight, 2008-07-30T10:00:00

Much of the output was produced by CXF. Only the last line was produced by the test program. As you can see, the major difference between the Groovy version and the Java version using CXF is that in Java, you will use the wsdl2java tool to generate the Java client proxy and the data contract classes up front; but in Groovy, all that is automatic when you run the script. This makes the Groovy code very simple and small. However, the downside of this is that the IDE (Eclipse) cannot know the methods and members of the class up front. Therefore, there is no code-completion (intellisense) support for the business methods. Since I heavily rely on code-completion of the IDEs, this is a big disadvantage about dynamic languages for me (I do not memorise all the class members and methods names, and I do not read all the API docs either. I usually try to figure out the methods I need by looking at their names).

There are a couple of things I had to do for my code to run without errors:

  1. I had to upgrade my JRE used by Eclipse to a 1.6.0_u10rc. Eclipse automatically addes a bunch of JARs from the JRE, including the resources.jar, which includes the JAXB classes. In an earlier version of my JRE (1.6.0_u2) the JAXB was version 2.0, but the CXF bundled in GroovyWS 0.3.1 requires v2.1. However, if you are not using Eclipse (e.g. using the Groovy Console directly), this step is not required
  2. The CXF bundled with GroovyWS 0.3.1 had problem generating the code from WSDL - it complained about a namespace "doesnt contain ObjectFactory.class or jaxb.index". It turned out to be a CXF bug and had been fixed in CXF 2.1.1, thanks to a post by Bernie. After I replaced the org/apache/cxf branch of the groovyws-all-0.3.1.jar using the contents in cxf-2.1.1.jar, it was smooth sailing.

Related Posts:

Saturday, 2 August 2008

RESTful Web Service

I built a SOAP web service using WCF as a part of a demo. However, as it turned out, the client software I was given can only handle HTTP as transport (the WCF service I did accepts requests directly from TCP payload). So I had to convert it into a RESTful web services. I have basicly a couple of options to implement the RESTful web services:

  1. Modify the existing WCF project to support REST style according to MSDN
  2. Create a ASP.NET project and reuse the code that I created in the WCF project.
  3. Create a Java web project and rewrite the code.

The 1st option seems the best choice on the surface. But there are still too much of hand-rolling of boiler-plate type of code to be done in option 1. You have to process the HTTP request method using if/switch ... yuck.

The 2nd option will save me some time because I can reuse the LINQ to XML code. But the code is very short, so the saving is not big. Again, the disadvantage of this option is that I have to create all the virtual path mapping (extending the VirtualPathProvider class) myself. The time to spend on building the ASP.NET web application from scratch will outweigh the savings on reusing the bit of XML query code (although there is a pretty good sample by Doug Seven from where I can steal some code.)

NetBeans (v6.1) has a RESTful web services (JSR-311) plugin using Jersey libraries. The wizard can create all the boilerplate codes and configurations required to get a RESTful service up and running by a few simple steps. The downside of this approach is that I have to rewrite the web service implementation methods - XML query, HTML scraping... However, the code for those is quite simple and should be easy to rewrite.

So, ASP.NET is lagging behind in the area of RESTful web services. I guess to get the similar features offered by JSR-311 and NetBeans, one has to wait for the upcoming MVC Framework for ASP.NET. An excellent tutorial and example project using the MVC Framework can be found here.

Since time is of essense in this exercise, I chose option 3.

I started by creating a Java Web Application (called SvdemoRestful) in NetBeans. Then adding a RESTful web service is simply right-clicking the project and choose New -> RESTful Web Services from Patterns... NetBeans also offers a wizard to create RESTful Web Services from Entity Class. I did not need to use Entity Class because I am going to use XPath to query a simple XML file (as my database) and return the same XML snippet to the service consumer (i.e. I don't even need to convert the XML into a Java bean);and I don't want to use the ORM tools and create Persistent Objects. Then it's just a matter of filling in the self-explanatory fields in the wizard.

Once this is done, the skeleton code is generated along with all the necessary web configuration settings (e.g. url mapping). I want to have my RESTful service to have URI like below so that the GET method will return the first promotion that matches the keywords (actually, it is better to use query parameters instead of path parameters for keyword searching services):

http://hostname/SvdemoRestful/resources/promo/jazz, action

To achieve this, the @Path annotation is defined as

@Path("promo/{keywords}")

I used XPath to retrieve the Promotion record from the data.xml file. I need to make the keyword matching case in-sensitive. In XPath 2.0 there is an upper-case() function, but it does not seem to be supported by the XPath libraries. So I have to use the following XPath statement:

//Promotion[contains(translate(Tags,'abcdefghijklmnopqrstuvwxyz','ABCDEFGHIJKLMNOPQRSTUVWXYZ'),'keywordsToBeMatched')]

as opposed to

//Promotion[contains(upper-case(Tags),'keywordsToBeMatched')]

So the resulting code for the promo RESTful web service:

/*
 *  PromoResource
 *
 */

package svdemo;

import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ProduceMime;
import javax.ws.rs.ConsumeMime;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.GET;
import javax.ws.rs.PathParam;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Node;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;

/**
 * REST Web Service
 *
 * @author ROMENL
 */

@Path("promo")
public class PromoResource {
    static String EMPTY="";
    @Context
    private UriInfo context;

    /** Creates a new instance of PromoResource */
    public PromoResource() {
    }

    /**
     * Retrieves representation of an instance of svdemo.PromoResource
     * The URL parameters are:
     * keywords - comma delimited words to be matched
     * @return an xml string representing the PromoInfo
     */
    @GET
    @Path("{keywords}")
    @ProduceMime("text/xml")
    public String getXml(@PathParam("keywords") String keywords ) {
        if (keywords == null || keywords.length()==0) {
            return EMPTY;
        }
        String[] words = keywords.split(",");
        try {
            DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
            domFactory.setNamespaceAware(true);
            DocumentBuilder builder = domFactory.newDocumentBuilder();
            Document doc =  builder.parse("data.xml");
            
            XPathFactory factory = XPathFactory.newInstance();
            XPath xpath = factory.newXPath();
            for(String keyword : words) {
                XPathExpression expr = xpath.compile(
                        "//Promotion[contains(translate(Tags,'abcdefghijklmnopqrstuvwxyz','ABCDEFGHIJKLMNOPQRSTUVWXYZ'),'"
                        +keyword.trim().toUpperCase()+"')]");
                Node result = (Node)expr.evaluate(doc, XPathConstants.NODE);
                if(result!=null) {
                    StringBuffer sb=new StringBuffer("");
                    NodeList nodes = result.getChildNodes();
                    for(int i=0; i<nodes.getLength(); i++) {
                        Node node = nodes.item(i);
                        if(!"#text".equals(node.getNodeName())){
                            sb.append("<"+node.getNodeName()+">");
                            sb.append(node.getTextContent());
                            sb.append("");
                        }
                    }
                    sb.append("");
                    return sb.toString();
                }
            }
            return EMPTY;
        } catch (Exception ex) {
            Logger.getLogger(PromoResource.class.getName()).log(Level.SEVERE, null, ex);
            return EMPTY;
        }
    }

    /**
     * PUT method for updating or creating an instance of PromoResource
     * @param content representation for the resource
     * @return an HTTP response with content of the updated or created resource.
     */
    @PUT
    @ConsumeMime("application/xml")
    public void putXml(String content) {
    }
}
In another service I needed to pass in a URL so that the service will get the web page of the URL and scrape the HTML code (yes, scraping HTML is a bad practice, but I don't have any alternatives and it's OK for demo purposes). The HTML scraping is easily handled by using the javax.swing.text.html.HTMLEditorKit. The problem is that the input parameter to this RESTful service contains forward-slashes ('/') and it will confuse the web server if I give the URLs like so:
http://hostname/SvdemoRestful/resources/webPageKeywords/http://localhost/someWebPage.html
http://hostname/SvdemoRestful/resources/webPageKeywords/'http://localhost/someWebPage.html'
According to JSR-311, the above urls should be specified as @Path("webPageKeywords/{url:.+}"), but it did not work. I tried different values of the limited and encode attributes of the @Path(), but to no avail. So I had to resort to passing the parameters as the URL's query parameter:
http://hostname/SvdemoRestful/resources/webPageKeywords?url=http://localhost/someWebPage.html
The corresponding @Path annotation is just @Path("webPageKeywords"). The corresponding service implementation code snippet:
/**
 * REST Web Service
 *
 * @author ROMENL
 */

@Path("webPageKeywords")
public class WebPageKeywordsResource {
    @Context
    private UriInfo context;

    /** Creates a new instance of WebPageKeywordsResource */
    public WebPageKeywordsResource () {
    }

    /**
     * Retrieves representation of an instance of svdemo.WebPageKeywordsResource 
     * @return empty string if no keywords found; otherwise, comma delimited keywords
     */
    @GET
    @ProduceMime("text/plain")
    public String getText(@QueryParam("url") String urlString) {
        if(urlString==null || urlString.length()==0)
            return "";
        try {
            URL url = new URL(urlString);
            HTMLEditorKit kit = new HTMLEditorKit();
            HTMLDocument doc = (HTMLDocument) kit.createDefaultDocument();
            doc.putProperty("IgnoreCharsetDirective", Boolean.TRUE);
            Reader HTMLReader = new InputStreamReader(url.openConnection().getInputStream());     
            kit.read(HTMLReader, doc, 0);
            String keywords="";
            Element e=doc.getElement("ProfileMusic");
            if(e!=null) {
                keywords=doc.getText(e.getStartOffset(), e.getEndOffset()-e.getStartOffset());
            }
            e=doc.getElement("ProfileFilms");
            if(e!=null) {
                keywords+=","+doc.getText(e.getStartOffset(), e.getEndOffset()-e.getStartOffset());
            }
            return keywords;
        } catch (Exception ex) {
            Logger.getLogger(WebPageKeywordsResource .class.getName()).log(Level.SEVERE, null, ex);
            return "";
        }
    }

    /**
     * PUT method for updating or creating an instance of WebPageKeywordsResource 
     * @param content representation for the resource
     * @return an HTTP response with content of the updated or created resource.
     */
    @PUT
    @ConsumeMime("text/plain")
    public void putText(String content) {
    }
}
I am looking forward to the new ASP.NET MVC Framework and will definitely rewrite these services using it.

Friday, 25 July 2008

Consuming WCF Web Service Using Java Client

[Updated on 2009-06-22] The JSE's native wsimport tool has been added along with custom binding in a more recent post - JAXB Custom Data Binding.

From the previous post I showed how to create a simple web service using WCF and consume it with a windows console application written in C#.

To have the same web services consumed by a Java client, I needed to make the web service WS-I Basic Profile 1.1 compliant so that it becomes portable cross-platforms. To do this, it is simply a matter of following instructions on MSDN. In my case, I modified Web.Config file: in <services> section I changed the wsHttpBinding into basicHttpBinding and removed the identity tags. The resulting snippet:


    
        
        
        
        
    

IntelliJ IDEA

To generate the Java web service client, I created a Java project in IDEA 7.0.2. Then right-click on the src tree to select New -> Web Service Client, then paste in the wsdl url (http://localhost/PromoService.svc?wsdl). Bingo, IDEA generates all the interface and class files, together with the client test code. I guess it is using JAXB under the hood. Adding the bits into the client test code resulted:

import svdemo.PromoService;
import svdemo.PromoInfo;

public class SvdemoClient {
  public static void main(String[] argv) {
      svdemo.IPromoService service = (new PromoService()).getBasicHttpBindingIPromoService();
      //invoke business method
      String keywords = "action";
      if(service.hasPromo(keywords)) {
          PromoInfo promo = service.getFirstPromo(keywords);
          System.out.println("promo:"+promo.getPromoName().getValue()
                  +","+promo.getPromoDateTime());
      }   else {
          System.out.println("nothing");
      }
  }
}

Notice how the service is instantiated (code not as nice as in C# version). Also, the PromoInfo class's members data type are of JAXBElement and XMLGregorianCalender..., which are not as nice as native primitive types. But it works - here is the output:

promo:Batman The Dark Knight,2008-07-30T10:00:00

NetBeans 6.0

I created a Java application project in NetBeans. From the project's source branch, right-clicking will bring up the New -> Web Service Client option. Providing the WSDL URI and selecting JAX-WS style will generate the WS client classes. The results are similar to IDEA, the exact same client code (see above) can be used to test the client. However, it is extremely slow so that I thought NetBeans froze. This happens to almost every step onwards. I eventually gave up on NetBeans. Maybe it is time to upgrade.

CXF

Apache CXF is the 2nd generation XFire. By default, it generates JAXB style binding. It came with wsdl2java utility. To generate the client code from wsdl, I used the following commands:

mkdir d:\projects\svdemocxf
wsdl2java -d d:\projects\svdemocxf -client -ant http://localhost/PromoService.svc?wsdl
I then moved all the source files into a src directory. Then build the project.
D:\projects\svdemocxf>ant build
D:\projects\svdemocxf>ant IPromoServiceClient
Buildfile: build.xml

compile:
    [javac] Compiling 15 source files to D:\projects\romen\build\classes

IPromoServiceClient:
     [java] Invoking hasPromo...
     [java] hasPromo.result=false
     [java] Invoking getFirstPromo...
     [java] getFirstPromo.result=null
     [java] Invoking getKeywordsFromMySpace...
     [java] getKeywordsFromMySpace.result=

BUILD SUCCESSFUL
Total time: 4 seconds

Note that the environment variable CXF_HOME had to be defined and point to the CXF installation directory for the ant build.xml file to work.

I added the client testing code similar to the one above:

package svdemo;
import org.datacontract.schemas._2004._07.svdemo.PromoInfo;
import org.tempuri.*;

public class Program {

 /**
  * @param args
  */
 public static void main(String[] args) {
  IPromoService service=(new PromoService()).getBasicHttpBindingIPromoService();
  String keywords="action";
  if(service.hasPromo(keywords)) {
   PromoInfo promo=service.getFirstPromo(keywords);
   System.out.println("promo:"+promo.getPromoName().getValue()
     +","+promo.getPromoDateTime());
  } else {
   System.out.println("nothing");
  }
 }
}
To run the test, I added the following target to the build.xml file:

    
 

Running the RunTest target yields:

D:\projects\svdemocxf>ant RunTest
Buildfile: build.xml

compile:
    [javac] Compiling 16 source files to D:\projects\svdemocxf\build\classes

RunTest:
     [java] promo:Batman The Dark Knight,2008-07-30T10:00:00

BUILD SUCCESSFUL
Total time: 4 seconds

Axis2

Apache Axis2 (v1.4) comes with the wsdl2java utility which generates java source code from WSDL file. Using the following command:

wsdl2java.bat -o \projects\svdemoAxis -p svdemoClient -uri http://localhost/PromoService.svc?wsdl

This generates the client stub class PromoServiceStub and dozens of inner classes - one for each input/output parameter and data type! The client test code ends up like this:

package svdemoClient;

import java.rmi.RemoteException;
import org.apache.axis2.AxisFault;
import svdemoClient.PromoServiceStub.PromoInfo;

public class Program {

 /**
  * @param args
  */
 public static void main(String[] args) {
  try {
   PromoServiceStub service=new PromoServiceStub();
   String keywords = "action";
   
   PromoServiceStub.HasPromo hasPromoPara=new PromoServiceStub.HasPromo();
   hasPromoPara.setKeyword(keywords);
   PromoServiceStub.HasPromoResponse hasPromoResp = service.HasPromo(hasPromoPara);
   if(hasPromoResp.getHasPromoResult()) {
    PromoServiceStub.GetFirstPromo firstPromoPara=new PromoServiceStub.GetFirstPromo();
    firstPromoPara.setKeyword(keywords);
    PromoServiceStub.GetFirstPromoResponse firstPromoResponse = service.GetFirstPromo(firstPromoPara);
    PromoInfo promo = firstPromoResponse.getGetFirstPromoResult();
    System.out.println("promo:"+promo.getPromoName()
      +","+promo.getPromoDateTime().getTime());
   } else {
    System.out.println("nothing");
   }
  } catch (Exception e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  
 }

}
Compiling and running it in Eclipse 3.4 yields:
promo:Batman The Dark Knight,Wed Jul 30 10:00:00 EST 2008
As shown above, the code is very UGLY! For each input parameter and output response, a class has to be instantiated (even when the parameter is simply a string). This not only makes the code long, but also very hard to read. This is the ADB binding used by Axis2 by default. Changing the binding style into xmlbeans or jibx slightly improves the esthetic and readability of the code, but not much. The following test client code snippet is written against XMLBeans binding style from Axis:
String keywords = "action";
HasPromoDocument hasPromoDoc = HasPromoDocument.Factory.newInstance();
hasPromoDoc.getHasPromo().setKeyword(keywords);
HasPromoResponseDocument hasPromoResp;
hasPromoResp = service.HasPromo(hasPromoDoc);

if(hasPromoResp!=null && hasPromoResp.getHasPromoResponse().getHasPromoResult()) {
 GetFirstPromoDocument firstPromoDoc=GetFirstPromoDocument.Factory.newInstance();
 firstPromoDoc.getGetFirstPromo().setKeyword(keywords);
 GetFirstPromoResponseDocument firstPromoResp=
  service.GetFirstPromo(firstPromoDoc);
 if(firstPromoResp!=null) {
  PromoInfo promo=firstPromoResp.getGetFirstPromoResponse().getGetFirstPromoResult();
  System.out.println("promo:"+promo.getPromoName()
    +","+promo.getPromoDateTime().getTime());
 }
}
This article from DeveloperWorks gives more insights and examples on the different types of Axis2 bindings. As of Axis2 v1.4, it came with a JAXB experimental support. The above is just my feeble attempt on Axis2. However, Axis2's wsdl2java is quite capable of generating esthetic code - Eclipse 3.4 uses Axis under the hood (using JAXB I guess) to generate web service clients.

Eclipse Ganymede JEE Distribution

Eclipse Ganymede (v3.4) has an option to add new Web Service Client. However, it requires you to specify a Server - this is totally wrong, there is absolutely no need to have a server definition to generate WS client code. However, I just played along and defined a Tomcat 6.0 server. It allows me to continue with the WS Client wizard. Then, after I specified the WSDL URI, it gave me the error:

IWAB0503E Unable to update Java build path. Please check your system environment.
    Java Model Exception: Java Model Status [Build path contains duplicate entry: 'D:tools/eclipse/plugins/org.apache.axis_1.4.0.v200806030120/lib/axis.jar' for project 'svdemoAxis']
    ...
Days later, I upgraded one of the Eclipse plugins (which was auto-detected and suggested by Eclipse), then things were better. Retrying generating the web service client resulted in a new WebServiceProject project being created. I then added the testing client:
package svdemoClient;
import java.rmi.RemoteException;

import org.datacontract.schemas._2004._07.svdemo.PromoInfo;
import org.tempuri.*;

public class Program {

 /**
  * @param args
  */
 public static void main(String[] args) {
  String keywords="action";
  IPromoService service=(new IPromoServiceProxy()).getIPromoService();
  try {
   if(service.hasPromo(keywords)) {
    PromoInfo promo=service.getFirstPromo(keywords);
    System.out.println("promo:"+promo.getPromoName()
      +","+promo.getPromoDateTime().getTime());
   }
  } catch (Exception e) {
   e.printStackTrace();
  }
 }

}
Note that the promotDateTime field generated is of java.util.Calendar type, not XMLCalendar.

Related Posts:

Thursday, 24 July 2008

WCF In 1 Hour

As part of a product concept study, I needed to prototype a web service to be consumed by a telephony service. This is to simulate a targeted promotion campaign in the telco environment.

The web service allows the consumer to check by keyword for any related promotions. If there are any, the consumer can retrieve the promotion and use the details of the promotion information - e.g. send a SMS or email to the subscriber to notify him/her about the promotion. I had the following in mind about the promotion web service:

  1. to store the promotion data in an xml file
  2. have a boolean service to check whether there are any promotions in the database with matching keyword
  3. have another service method to return the promotion data (an object/composite type) with matching keyword
  4. the promotion information need to be short and concise, making it suitable for sending in a SMS
The XML file I chose to create looked something like this:

 
  New Years Eve Concert
  The biggest event of the year. Artists from all around
  the country.
  Domain
  2008-12-31 20:00:00
  New Year's Eve
 
 
  Batman The Dark Knight
  
   Meet stars in Batman in person - Chritian Bale, Michael Caine.
  
  Star City
  2008-7-30 10:00:00
  Batman, action
 
 
  Jazz Night
  
   Jazz lovers' do not miss this once in a lifetime opportunity.
  
  The Jazz Club
  2008-10-30 21:00:00
  Jazz
 

After a short consideration, I decided to use .NET WCF for the web service and LINQ for the XML querying. The only problem is that I knew nothing about WCF and little about LINQ (I lived in the Java land). But it did not stop me. In fact, it made it more exciting.

The first thing I did was to create a WCF project in VS.2008. VS.2008 made it extremely easy. I then modified the auto-generated service interface and the service implementation files:

The serivce interface is IPromoService

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;

namespace svdemo {
    [ServiceContract]
    public interface IPromoService {
        /// 
        /// Tests whether there are any promotions with matching keyword.
        /// 
        /// <param name="keyword">the keyword to be matched. Case in-sensitive, single word only</param>
        /// <returns>whether there are any promotions with matching keyword</returns>
        [OperationContract]
        bool HasPromo(string keyword);

        /// 
        /// Get the first promotion which matches the given keyword.
        /// 
        /// <param name="keyword">case in-sensitive, single word only.</param>
        /// <returns>The first promotion with matching keyword.</returns>
        [OperationContract]
        PromoInfo GetFirstPromo(string keyword);
    }


    // Use a data contract as illustrated in the sample below to add composite types to service operations.
    [DataContract]
    public class PromoInfo {
        string promoName;
        string promoDescription;
        string promoVenue;
        DateTime promoDateTime;

        public PromoInfo(string name, string desc, string venue, DateTime dt) {
            promoName = name;
            promoDescription = desc;
            promoVenue = venue;
            promoDateTime = dt;
        }
        [DataMember]
        public string PromoName {
            get { return promoName; }
            set { promoName = value; }
        }

        [DataMember]
        public string PromoDescription {
            get { return promoDescription; }
            set { promoDescription = value; }
        }

        [DataMember]
        public string PromoVenue {
            get { return promoVenue; }
            set { promoVenue = value; }
        }

        [DataMember]
        public DateTime PromoDateTime {
            get { return promoDateTime; }
            set { promoDateTime = value; }
        }
    }
}

The service implementation PromoService:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.Xml.Linq;

namespace svdemo {
    public class PromoService : IPromoService {
        XDocument doc;
        public PromoService() {
            doc = XDocument.Load(@"D:\projects\SigvalueWS\svdemo\svdemo\data.xml");
        }

        public bool HasPromo(string keyword) {
            if (keyword == null)
                return false;

            string kw = keyword.ToUpper();
            var q = from p in doc.Descendants("Promotion")
                    where ((string) p.Element("Tags")).ToUpper().Contains(kw)
                    select p.Element("Name").Value;
            return (q.Count() > 0);
        }

        public PromoInfo GetFirstPromo(string keyword) {
            if (keyword == null)
                return null;
            string kw = keyword.ToUpper();
            var q = from p in doc.Descendants("Promotion")
                    where ((string) p.Element("Tags")).ToUpper().Contains(kw)
                    select new PromoInfo(p.Element("Name").Value,
                        p.Element("Description").Value,
                        p.Element("Venue").Value,
                        DateTime.Parse( p.Element("DateTime").Value));
            PromoInfo promo = q.First();
            return promo;
        }
    }
}

Running this project (Ctrl+F5) will deploy the web service onto a test web server and the wsdl can be obtained from the url http://localhost:2144/PromoService.svc?wsdl

To test the consumption of the web service, I created a console application project in C#. Adding a service reference (by copying the above URL into it) automatically generates the required web service client entry point code, client proxy, and the DTO PromoInfo class. So consuming the service is a no-brainer:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using svdemoClient.ServiceReference;

namespace svdemoClient {
    class Program {
        static void Main(string[] args) {
            PromoServiceClient client = new PromoServiceClient();
            string keyword="Jazz";
            if (client.HasPromo(keyword)){
                PromoInfo promo=client.GetFirstPromo(keyword);
                Console.WriteLine(
                    String.Format("got {0}, {1}", 
                    promo.PromoName, promo.PromoDateTime)
                );
            }else
                Console.WriteLine("nothing");
            System.Threading.Thread.Sleep(3000);
            client.Close();
        }
    }
}

Although this is my very first attempt to use WCF and LINQ to XML, the whole thing took me an hour and a bit and majority of the time was used in googling on how to use LINQ to XML.

Note: it is better to modify the transport setting to use Basic HTTP instead of TCP to improve interoperability. See here for more details.