Wednesday, September 17, 2008

Building JAX-WS web service

Last year I wrote a small step by step guide to build JAX-RPC web services. Now JAX-RPC has been replaced by new standard JAX-WS, so I thought it is good time to write an entry for JAX-WS as well. Building web services with JAX-WS is pretty straight forward though it might look cumbersome to a newbie. In this entry I am going explain the basic steps for building a Java first web service, which I developed and tested with JAX-WS 2.1.4, Java 6 update 6 and Tomcat 6.0.16.

Step #1 - Write an interface

@WebService(targetNamespace = "http://vinodsingh.com", name = "MyService")
@SOAPBinding(style = SOAPBinding.Style.DOCUMENT, use = SOAPBinding.Use.LITERAL)
public interface MyService {

    String sayHello(@WebParam(name = "name") String name);
}

Step #2 - Implement the interface
@WebService(endpointInterface = "pkg.MyService")
public class MyServiceImpl implements MyService {

    @Override
    public String sayHello(String name) {
        return "Hello " + name + "!";
    }
}

Step #3 - Configure web.xml

The JAX-WS context listener and servlet are required to be configured in deployment descriptor (web.xml). The WSServletContextListener initializes and configures the web service endpoint and WSServlet serves the service requests using implementing class.
<listener>
    <listener-class>
        com.sun.xml.ws.transport.http.servlet.WSServletContextListener
    </listener-class>
</listener>
<servlet>
    <servlet-name>jaxws</servlet-name>
    <servlet-class>com.sun.xml.ws.transport.http.servlet.WSServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>jaxws</servlet-name>
    <url-pattern>/myService</url-pattern>
</servlet-mapping>

Step #4 - sun-jaxws.xml

The JAX-WS RI uses information available in this file while initializing and configuring a web service endpoint. This file should be present in WEB-INF directory.
<endpoints
    xmlns='http://java.sun.com/xml/ns/jax-ws/ri/runtime'
    version='2.0'>
    <endpoint
        name='myService'
        implementation='pkg.MyServiceImpl'
        url-pattern='/myService' />
</endpoints>

Step #5 - Build and deploy

Here is an Ant script to prepare the server side stuff-
<!-- Define classpath. -->
<path id="jaxws.classpath">
    <pathelement location="${java.home}/../lib/tools.jar" />
    <fileset dir="${jaxws.lib.dir}">
        <include name="*.jar" />
    </fileset>
</path>

<!-- Declare ant tasks required to generate service and client. -->
<taskdef name="apt" classname="com.sun.tools.ws.ant.Apt" classpathref="jaxws.classpath" />
<taskdef name="wsgen" classname="com.sun.tools.ws.ant.WsGen" classpathref="jaxws.classpath" />
<taskdef name="wsimport" classname="com.sun.tools.ws.ant.WsImport" classpathref="jaxws.classpath" />

<!-- Run the 'apt' tool on annotated code to generate server side stuff. -->
<target name="buildService">
    <apt fork="true" destdir="${classes.dir}" 
         sourcepath="${src.main.dir}" 
         sourcedestdir="${gensrc.dir}" 
         debug="${debug}" verbose="${verbose}" classpathref="jaxws.classpath">
        <source dir="${src.main.dir}">
            <include name="**/MyServiceImpl.java" />
        </source>
    </apt>

    <jar destfile="${build.home}/server.jar" basedir="${classes.dir}" compress="true" />
</target>

Now package all this in a WAR file and deploy on the server. The web service can be accessed at http://localhost:8080/testws/myService (URL may change according to the server configuration).

Update Feb 13, 2009: With latest versions of Java 6 and JAX-WS, apt task is optional. See this comment below.

Step #6 - Generate client

And this script will generate client side stubs. Here first we are generating WSDL and then using that to generate client side stubs. Though we can avoid the WSDL generation and use the live URL by deploying the web service on a server but usually that is not feasible in a project where build process is automated.
<!-- Generate the client, for that first WSDL needs to be generated. -->
<target name="genClient" depends="buildServer">
    <wsgen sei="pkg.MyServiceImpl" 
        resourcedestdir="${gen.wsdl.dir}" 
        sourcedestdir="${gensrc.dir}/client" 
        destdir="${classes.dir}/client" 
        genwsdl="true" verbose="${verbose}" keep="true">
        <classpath>
            <pathelement location="${classes.dir}/server" />
        </classpath>
    </wsgen>

    <wsimport destdir="${classes.dir}/client" 
        sourcedestdir="${gensrc.dir}/client" 
        package="pkg" debug="${debug}" 
        wsdl="${gen.wsdl.dir}\MyServiceImplService.wsdl" 
        xendorsed="true" keep="true" />

    <jar jarfile="${build.home}/ws-client.jar" basedir="${classes.dir}/client" compress="true" />
</target>

Step #7 - Invoke the service

Here is the sample code to invoke the web service, which we just built and deployed.
// Get a handle to web service client interface
WebServiceClient ann = MyServiceImplService.class.getAnnotation(WebServiceClient.class);
MyServiceImplService service = new MyServiceImplService(new URL(
    "http://localhost:8080/testws/myService"), 
    new QName(ann.targetNamespace(), ann.name()));
MyService myService = service.getMyServiceImplPort();

// Invoke methods
String hello = myService.sayHello("Vinod");
System.out.println(hello);

Thats it. We are done. Source code of this example is available here.

Update Apr 12, 2010 - A new post JAX-WS Web service with Maven shows usage of Maven as build script for building JAX-WS web services.

Friday, September 05, 2008

AnyType object over webservice and Hibernate

Recently I came across a requirement of moving objects over the network using web services (JAX-WS 2.1). The application was CRUD in nature and Hibernate was used as data tier on the server. In case of web service usually we have WSDL, which clearly defines the kind of objects it expects in request and response. So we can have add/modify/delete/retrieve methods for each type of entity in the WSDL. This approach seems to be doable when we have very few entities. But in our case we were having too many entities and the WSDL was going to be very huge and repetitive in nature. Then what is the way out!!!

I created a base class say BaseEntity.java, which is just a place holder having no variables or methods. Each entity was made to extend BaseEntity.java. The methods in web service interface were having signature like-

@XmlSeeAlso( { MyActualEntity.class })
public interface DataService {

   public void add(BaseEntity object);
}
In the implementing class of the web service interface, Hibernate can determine the actual class name handles that properly to insert the data in correct table. So all seems to be set on the server side, what about client side?

After generating client side stubs when I tried to add an instance of MyActualEntity.java, JAXB complaint that it does not know anything about MyActualEntity. JAXB was absolutely right, it knows only about those data types, which are mentioned in WSDL and MyActualEntity was not part of the WSDL. Then how shall I tell the JAXB about the real entities :-/

We can add the complexType definitions for all entities in the WSDL, but lazy resisted that ;-)
After doing some research I found @XmlSeeAlso annotation., which can be applied on the web service interface. When wsgen task encounters the @XmlSeeAlso annotation, it will include those classes in the WSDL. Now JAXB knows about MyActualEntity and sends that across the wire.

Seems to be a good trick :-)

Tuesday, July 29, 2008

SVN with Apache + LDAP

We have been using CVS at work for long, though personally I am a big fan of SVN. Lately we thought to catch up with the SVN buzz and I was asked to have a Proof Of Concept before we officially move to SVN. Installation of SVN with Apache was pretty straightforward. Though LDAP integration was also simpler but at times it failed without any obvious reasons. Here are the steps for the entire process on Windows XP-

1. Install Apache 2.2.
2. Install SVN 1.5 by unzipping the distribution.
3. Add SVN\bin folder to PATH.
4. Copy mod_dav_svn.so to modules directory of Apache.
5. Add following line in Apache conf file-
LoadModule dav_module modules/mod_dav.so
LoadModule dav_svn_module modules/mod_dav_svn.so

# Following are required for LDAP authentication
LoadModule authnz_ldap_module modules/mod_authnz_ldap.so
LoadModule ldap_module modules/mod_ldap.so

6. Download [svnindex.css and svnindex.xsl] from http://svn.collab.net/repos/svn/trunk/tools/xslt/
and put them in 'htdocs' folder of Apache. This will do nice listing of SVN contents.

7. Add the repository location in http conf file, this will have LDAP auth as well

#Location of repository
<Location>
DAV svn

SVNParentPath C:\svn
SVNListParentPath on
SVNIndexXSLT "/svnindex.xsl"

AuthType Basic
AuthBasicProvider ldap
AuthzLDAPAuthoritative off
AuthName "My SVN Setup"
AuthLDAPURL ldap://host:3268/dc=X,dc=Y?sAMAccountName?sub?(objectClass=user) NONE
AuthLDAPBindDN myUserID
AuthLDAPBindPassword myPassword
Require valid-user
</Location>

7. Run 'httpd' command from Apache

VOILA! YOU ARE DONE

I gave it a try on Fedora 4 as well, which comes with Apache 2.2 and SVN 1.4 pre-installed. Only thing that was missing was there 'mod_dav_svn'. Getting that to with YUM is pretty simple, just do-
yum install mod_dav_svn

Here we were using Active Directory as our LDAP server. Initially I used the port 389 in the AUthLDAPURL shown above and it worked fine for first 2 weeks but after that it failed without any obvious reasons with this error message in Apache log files-
[ldap_search_ext_s() for user failed][Operations Error]

It made it to work for brief periods by re-starting the Apache. Re-starting is not an option in production. After googling for some time I found suggestions to use port 3268 instead of 389. More than 2 weeks has been passed since I switched to port 3268 and it is working fine till date :-)

My next step will be to convert some CVS repositories to SVN. Will update soon about the results of this conversion.