Showing posts with label SOA. Show all posts
Showing posts with label SOA. Show all 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.

Friday, 27 June 2008

SOA Pitfalls

The Top 10 SOA Pitfalls series is a really interesting read. It reminds me of my first hand experiences of some of those pitfalls.

I was in charge of the solutions for a high profile project in a large Telco environment. In this particular environment, there were myriads of small systems which did not talk to each other. Reason for the proliferation of many small systems is that each department head can approve projects below certain budget. So each time a department/group has some business requirements, the tendency is to have a chewing gum and duct tape solution as long as it stays under the budget limit for the department head. So after years of such practice, users were left with small systems that were small in functionality and an overall complex enterprise systems environment for any IT architects.

The client had come to their senses to change this by introducing a COTS. Part of our project's responsibility was to replace those small systems with the COTS. After initial study, I found that we had to replace 7 small systems and integrate with 17 others. In this environment, a SOA platform is called for. Fortunately the client had corporate license for SeeBeyond (now called JCAPS after Sun bought it). JCAPS had been used in several other high profile projects ($10Ms, 1-3 years implementation time). To my astonishment, each of those project had its own instance of JCAPS running; and these instances were not talking to each other. So it looked like our project was going to introduce another instance of JCAPS silo! This type of use of EAI defeats the purpose of using EAI. After all, the 'E' in EAI stands for 'Enterprise' - it is supposed to be enterprise wide. After some hard fighting, I am glad to say that the project had a happy ending.