Friday, May 16, 2008

Spring + xFire

     All the web service projects my team has been working on are using Axis. But one of the projects went complex and Axis cannot handle it any more. For one thing, some of the objects use java.sql.Timestamp instead of java.util.Date. Another thing is that we use java.util.List instead of Array, which's generic type cannot be specified in Axis configuration.

     So once again, I'm told to solve the problem. Sure it'll be easier just change the types from Timestamp to Date and from List to Array. But it won't be fun to do then. The company's own web service framework works with both Axis and xFire. Since Axis isn't working this time, I'm going to switch to plan B - xFire.

     Surprisingly, xFire is a lot easier to configure than Axis and has better support on Timestamp and List.

     First of all, you need to download xFire and all the dependencies.

Following is the configuration:
maven.xml

<project xmlns:j="jelly:core" xmlns:ant="jelly:ant">  
  <ant:property environment="env"/>

  <goal name="init" description="Set up the application for eclipse." >
    <attainGoal name="eclipse"/>
    <attainGoal name="populate-webinf-lib"/>
    <ant:echo>
      Now refresh in eclipse to pick up the changes 
    </ant:echo>
  </goal>

  <goal name="populate-webinf-lib">
    <mkdir dir="${maven.war.src}/WEB-INF/lib"/>
    <j:forEach var="pa" items="${pom.artifacts}">
      <j:set var="warDependency"
        value="${pa.dependency.getProperty('war.bundle')}"/>
      <j:choose>
        <j:when test="${warDependency == 'true'}">
          <echo>WAR dependancy: ${maven.repo.local}/${pa.urlPath}</echo>
          <copy file="${maven.repo.local}/${pa.urlPath}"
            toDir="${maven.war.src}/WEB-INF/lib"/>
        </j:when>
      </j:choose>
    </j:forEach>
    <echo>Copied WAR dependencies to WEB-INF/lib for local development.</echo>
  </goal>

  ...

</project>


web.xml:

...
<servlet>
    <servlet-name>XFireServlet</servlet-name>
    <servlet-class>
        org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>XFireServlet</servlet-name>
    <url-pattern>/servlet/XFireServlet/*</url-pattern>
</servlet-mapping>

<servlet-mapping>
    <servlet-name>XFireServlet</servlet-name>
    <url-pattern>/xfireservices/*</url-pattern>
</servlet-mapping>

...

<context-param>
    <param-name>contextConfigLocation</param-name>
    <!-- put all of your spring context files separated by a space here to load them up at startup-->
    <param-value>
        classpath:org/codehaus/xfire/spring/xfire.xml
        /WEB-INF/classes/config/applicationContext.xml
    </param-value>
</context-param>
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

...



xFireServlet-servlet.xml:

<beans default-lazy-init="true">
    <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/ServiceName">ServiceBean</prop>
            </props>
        </property>
    </bean>
</beans>


applicationContext.xml:

<beans default-autowire="byName">
    <bean name="ServiceBean"
            class="org.codehaus.xfire.spring.remoting.XFireExporter">
        <property name="serviceBean" ref="SpringBean" />
        <property name="serviceClass"
            value="com.mycompany.ws.IServiceBean" />
        <property name="serviceFactory" ref="xfire.serviceFactory" />
        <property name="xfire" ref="xfire" />
    </bean>
</beans>


     IServiceBean is an interface. Another bean named SpringBean needs to be configured which should implement IServiceBean, in this case, SpringBean is an instance of com.mycompany.ws.ServiceBeanImpl.

     To generate Client and Server Stub from WSDL file, you can refer to the official documentation here. Following is my configuration using maven.

maven.xml

<project xmlns:j="jelly:core" xmlns:ant="jelly:ant" >

  ...

  <goal name="wsdl2java" >
    <echo message="Generating client files from wsdl" />
    <java classname="org.codehaus.xfire.gen.WsGen" fork="true" failonerror="true">
      <arg value="-o"/><arg value="${basedir}/target/client"/>
      <arg value="-wsdl"/><arg value="${basedir}/conf/ServiceBean.wsdl"/>
      <arg value="-p"/><arg value="com.mycompany.webservices"/>
      <arg value="-overwrite"/><arg value="true" />
      <classpath refid="maven.dependency.classpath"/>
    </java>
  </goal>
</project>


     Note that I have a local copy of the WSDL file instead of a URL. It's because the URL I'm using is requiring authentication which fails me every time no matter how I set it up. Same thing happens when I run Unit Test. It should be a problem with xFire. But since xFire has merged with CXF, I have no clue whether they are gonna fix it or not.

     Finally, we can do a little Unit Test.

ServiceTestCase.java

package com.mycompany.test;

import junit.framework.TestCase;

import org.springframework.beans.BeansException;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.util.StringUtils;

import com.mycompany.ws.IServiceBean;

public class ServiceTestCase extends TestCase{
    
    protected ClassPathXmlApplicationContext context = null;

    protected IServiceBean service = null;

    protected static String LOCATION = "config/applicationContext-test.xml";
   
    public ServiceTestCase(final String name){
        super(name);
    }    

    /* (non-Javadoc)
     * @see junit.framework.TestCase#setUp()
     */
    @Override
    protected void setUp() throws Exception {
        context = new ClassPathXmlApplicationContext(StringUtils.commaDelimitedListToStringArray(LOCATION), true);
        service = (IServiceBean) context.getBean("testWebService");
    }

    ...
    //write tests here
    ...
}


applicationContext-test.xml

<beans>
  <bean id="testWebService"
      class="org.codehaus.xfire.spring.remoting.XFireClientFactoryBean">
    <property name="serviceClass" value="com.mycompany.ws.IServiceBean" />
    <property name="username" value="username" />
    <property name="password" value="password" />
    <property name="wsdlDocumentUrl" value="[my WSDL file location]" />
  </bean>
</beans>