Implement a Web Service using Axis, Ant and Tomcat

Author: Luciano Fiandesio (lfiandesio@yahoo.com)

Version: 0.1 (May 2004)

Location: http://www.lucianofiandesio.com/javatales/axant.html

Source code location: http://www.lucianofiandesio.com/javatales/axis-tutorial.zip


Introduction

Recently, I have been asked to integrate the J2EE application I was working on with a FAME system (http://www.fame.com). FAME is basically a database for managing, analyzing and presenting time series data and it is widely used in the financial sector. Excluding some basic Java APIs, it does not offer any integration facility. The most important aspect of this integration was the possibility to exchange file between our J2EE based application and the FAME environment.

In particular the requirements were:

  1. Send procedures and data to FAME (a procedure is a simple ascii file containing code that FAME can compile and run)

  2. Execute the procedures inside the FAME environment

  3. Asynchronously retrieve the results of the executed procedures (returned files are in different formats, PDF, Poscript, ascii).

After meeting the FAME team, we decided to implement the integration as a Web Service, having Apache Axis on “our” side and a Perl implementation (Perl:lite) on the FAME side. The decision to use Perl on the FAME side was taken because the FAME guys talked no Java and they were skilled mainly in Perl. Furthermore, they already had many Perl scripts to run FAME procedures in place.

Unfortunately we soon discovered that the Perl implementation has still many bugs and the organization I was working for did not like to use such a fragile solution. So we choosed to place Apache Axis also on the FAME side, making things much easier (and giving a chance to the FAME guys to talk some Java).

Last but not least, we needed a simple, short-timed solution easily integrable in a “continuous integration” scenario. Most of the web service code had to be Ant-generated, in order to minimize coding efforts and reduce the need for support. Furthermore, the possibility to add new functionalities to the web service solely adding methods to an interface was very attractive in terms of code maintenance.

Please note that this tutorial will not go into the basics of a Soap web service. The Internet has plenty of documentation and many books has been written on the subject (see Resources).

Environment set-up

Implementation

The first part of the job was to agree on the Web Service interface.

The interface looked like this:


Public interface FAMEService {

public String executeFameProcedure(FAMEArgumentsTO argument);

public FAMEExecutionResultsTO retrieveResult(String correlationId);

}


Actually the interface was a little more complex than this, but this is enough to show how to build a Web Service with a complex data type as parameter and how to use attachments with “Soap with Attachments”.

The first method, executeFameProcedure, is used to run a FAME script against the FAME runtime environment. The argument is a complex object (a TO, or Transfer Object) that transports the ascii file needed to execute the scripts. FAME invocations are asynchronous. FAME requires some time to run the script and produce results. That is why the return type is a “Correlation ID”, used in subsequent invocations to retrieve these results (if any).

The second method, retrieveResult, collects the results from a previous FAME execution.

Results can be ascii files or binary files (PDF or Postcript files). The files are transferred using another TO as return parameter.

The name of the web service for this tutorial is “fameLounge”. The name is important because it is the “entry point” of the service itself.

The core of the Web Service is a Java class called FAMECommandExecutor. This class is responsible of calling different Perl scripts to run procedures and retrieve results. In this specific case, this class was provided by the FAME team. Our Web Service have to invoke some methods of this class to make its job.


Before starting to expose our code as a Web Service we must be sure that the code works and it is decently bugs free. It means that some test framework should be used to run tests against the code that will be (directly or indirectly) exposed as a Web Service.

The Ant build script

A key element of working with Axis is to use a Ant build script capable of performing most of the job.

An Axis Ant script should perform the following operations:

We start with a classic Ant project structure:

bin
build
dist
lib
src


Let’s dig into the Ant build xml script:


<target name="build" description="">

<mkdir dir="${build.dir}"/>

<javac destdir="${build.dir}" target="1.4" debug="true"

deprecation="false" failonerror="true">

<src path="${src.dir}"/>

<classpath refid="master-classpath"/>

</javac>

</target>


This is the first task, the Web Service code compilation. The classpath reference should point to the lib folder where Axis and other required libraries are placed.

<target name="generate-wsdl">
<delete file="${wsdl.name}"/>

<axis-java2wsdl

classname="${endpoint.package.structure}.${wsdl.class.name}"

style="wrapped"

namespace="${service.namespace}"

location="http://${host}:${port}/axis/services/${service.name}"

output="${wsdl.name}">

<classpath refid="compiled.class.path"/>

</axis-java2wsdl>

</target>


This is the task that generates the WSDL file. The Axis distribution comes with some nice Ant-tasks that perform most of the tedious work, such as generating a WSDL against a Java interface.


Most SOAP purists will not agree with this approach. To obtain maximum interoperability it is normally suggested to start creating the WSDL itself and then write the implementation using the chosen language. Generating a WSDL from a Java class (or any other language) may cause a language specific XML type mapping, preventing a client to interoperate correctly with a web service written in a different language. In this specific scenario, our requirements did not include interoperability with other platforms.


The axis-java2wsdl task it’s just what we need for generating a WSDL file. This task requires some parameters. In this case I used:

classname: specify the full name of the class that will be used to generate the WSDL. I experienced some problems generating the WSDL against the interface. I use the class implementation to generate the WSDL.

style/use: RPC/encoded, RPC/literal or document/literal. Remember that the WS-I Basic Profile permits the use of RPC/ Literal or Document/Literal only. The de-facto standard is “document/literal” (or “wrapped” document/literal) because of its compatibility with .NET and because of the WS-I Basic Profile.

namespace: the name of the target namespace of the WSDL. In this case “urn:FAME”.

location: the location of the service. Something like http://localhost:8080/axis/services/fameLounge

output: the name of the output WSDL file


It is a good rule to use properties as much as possible when dealing with Ant based script. That is why most of the properties have parameterized values, that can be stored on the same build file or on an external properties file (or set as using the –D flag).


Once the WSDL file has been generated, it is time to generate the WSDD and the client proxy classes. The generated client (or proxy) Java code is basically the client implementation of the web service and can be used with almost no modifications to invoke the web service.


<target name="generate-wsdd">

<mkdir dir="${build.dir.axis}"/>

<axis-wsdl2java

output="${build.dir.axis}"

verbose="false"

url="${wsdl.name}" serverside="true"

debug="false">

<mapping namespace="${service.namespace}"

package="${generated.package.structure}"/>

</axis-wsdl2java>

</target>


What is a WSDD file? It is basically a deployment descriptor in XML format. WSDD files are unique to Axis and describe the web service, which methods are exposed and so on. The WSDD file will be used to deploy and undeploy the web service.

Using the axis-wsdl2java Ant task avoid the hassle of hand-write this file (as a general rule, it would be always better to know what your tools are going to generate, especially in the case of a WSDL file).

What about the parameters used in this task?

output: (from the Axis manual) output directory for emitted files.

url:the location of the WSDL file. The previously generated WSDL file is referenced here.

serverside: if set to “false” the WSDD files are not emitted, than I set it to “true”

The “mapping” element is a little tricky to grasp. If the “mapping” element is not specified, this Ant task will emit the client code and the WSDD files setting the package name to the namespace of the service (in this case “FAME”):

package FAME;
public class FAMEServiceLocator {
...


We want to preserve the package structure. By specifying the “mapping” attribute we can basically map the namespace to a package structure. The actual value of the attribute {generated.package.structure} is com.javatales.axis.gen and the produced code will look like:

package com.javatales.axis.gen;
public class FAMEServiceLocator {
...

With the server-side option, WSDL2Java also generates Java source files for:



At this stage of the build process, we almost have all the necessary client and server code to deploy and run the web service. If we interrupt the build process at this stage, we would notice two new directories into our main build folder:

  1. .classes

  2. axisAutoSource

The “.classes” directory contains the compiled Java code for the whole project. This is the result of the first Ant task we took in consideration (build).

The “axisAutoSource” directory has been created by the last task generate-wsdd and contains the client code used to invoke the web service plus the WSDD files to deploy/undeploy the web service. This directory also contains a “to” directory, very similar to the original “to” directory of the “src” folder. The original “to” folder contains the Transfer Objects classes used as input and output parameters of the web service. This new “generated to” folder contains the same “to”s classes revamped by the Axis emitter (it is easy to directly see the differences between the two sets of classes). The Axis task re-generate these classes because they are specified as parameters in the WSDL file. Unfortunately, the emitter proved to be buggy, so later we will overwrite these classes with the original ones.


The next step of the build process is the (re)compilation of the original web service code and the generated client code.


<target name="compile-axis-generated">

<mkdir dir="${build.final}"/>

<copy overwrite="true" todir="${build.dir.axis}">

<fileset dir="${src.dir}">

<exclude name="**/*Test*.java"/>

</fileset>

</copy>

<javac destdir="${build.final}" target="1.4" debug="true"

deprecation="false" optimize="false" failonerror="true">

<src path="${build.dir.axis}"/>

<classpath refid="master-classpath"/>

</javac>

<echo message="Applying classname substitution..."/>

<replaceregexp

file="${build.dir.axis}/com/javatales/axis/gen/deploy.wsdd"

match="${generated.package.structure}.FameLoungeSoapBindingImpl"

replace="${endpoint.package.structure}.${wsdl.class.name}"

/>

</target>


This task performs three main activities.

  1. Copies all the Java source code from the “src” folder to the “axisAutoSource” folder. Copying the files insure that the final compiled code is the original one and not the Axis generated one (the “to” classes mentioned before). The only exception is the client code, that will remain untouched.

  2. Compiles all the code, both client (Axis generated) and server.

  3. Substitutes the Axis generated implementation class with the real web service class implementation in the WSDD file. The Axis generated WSDD deployment file contains a wrong mapping to the actual implementation class of the web service.

    <parameter name="className" value="com.javatales.axis.gen.FameLoungeSoapBindingImpl"/>
    became
    <parameter name="className" value="com.javatales.axis.FameServiceImpl"/>


    The WSDD emitter does not know that we have a service interface and implementation already in place. It generates the WSDD from a WSDL file and automatically creates also the interface and a skeleton implementation.

At this point of the build process, we only need to “jar” the code and deploy it in Tomcat.

The “jar” task is trivial. In a real world scenario, it may be better to divide the jars files into client and server, where the client jar contains only client related code and the server only the code that will run as a “service”.

In order to deploy (and undeploy) the web service I used the Ant tasks that comes with Tomcat. The usage is straightforward and the build.xml file that uses as reference for this tutorial should be enough to understand how to play with those tasks.

Please remember that the deploy task only works on a local Axis installation.


Invoking the web service

At this point we should have a deployed web service waiting to be invoked (assuming that Axis was installed). In order to check that our service is deployed, we can open a web browser and point to the address of the running Tomcat instance:

http://127.0.0.1:8080

If the server replies with a welcome page we can proceed further to:

http://127.0.0.1:8080/axis/

and then click on “View the list of deployed web service

Among the other deployed web services, we should see the “fameLounge” web service, with a hyper link to the “wsdl”. Clicking on the “wsdl” link should show the actual web service WSDL xml code.

If everything went smooth until here, the web service should be up and running.

There are different approaches to test a web service. For sake of simplicity I will go with the simplest one, a very basic Java class. The class does not extend Junit, but can be easily customized to do so.


The first method to test is executeFameProcedure. As I said before, we need to to stub the real FAMECommandExecutor class. The dummy FAMECommandExecutor exposes an interface equal to the real one, but the code behaves differently. The executeProcedure method of the FAMECommandExecutor simply get the parameters (files) and write them to a temporary directory. Doing so, we can test if the client is able to send files to the service and the service can handle them correctly.

Let’s look at the signature of the executeFameProcedure method exposed by the web service implementation:

public String executeFameProcedure(FAMEArgumentsTO to);

The method has only one parameter and returns a String. The parameter is a POJO class and has some attributes to transfer procedures files and datasets. A procedure file is an ascii file containing some FAME code to be executed. Datasets are xml files that FAME will use as arguments for the passed procedure. The returned argument is a String containing a “correlation ID”. The correlation ID is a marker for a FAME execution process. As said, FAME is invoked asynchronously and the results of the elaboration have to be retrieved using a different method that requires this correlation identifier.


The FAMEArgumentsTO class:


public class FAMEArgumentsTO {

private FAMEProcedureTO mainProcedure;

public DataHandler[] datasets;

public FAMEProcedureTO getMainProcedure() {

return mainProcedure;

}

public void setMainProcedure(FAMEProcedureTO procTO) {

mainProcedure = procTO;

}

}


This POJO has a couple of things that require some explanation. The “main procedure” variable is exposed as a type FameProcedureTO. It means that also this property is exposed as a POJO. With Axis it is possible to aggregate classes in order to maintain a clean OO design.

The second property is an array of DataHandler objects.The javax.activation.DataHandler object comes from the JavaBeans Activation Framework. It can automatically detect the data type passed to it, and it can therefore automatically assign the appropriate MIME content type to the attachment. Normally the DataHandler class is used to transport binary files outside the SOAP message body (using the MIME multipart/related specification). Originally this POJO was exposing the datasets property as an array of String. This was causing some memory related issues. The dimension of a dataset may vary between few kilobytes to a couple of megabytes (or more). In the case of huge datasets file, the Axis engine was causing a outOfMemoryException in Tomcat. The problem probably resides in the XML parser, but we did not have time to investigate. Using a DataHandler object automatically “moves” the dataset files outside the Soap body message, resolving the memory leak issues.

The Axis mailing list contains many warnings regarding the usage of the DataHandler class and interoperability with other platforms (in particular, .NET). The DataHandler class is simply not recognized by the .NET Soap implementation. Some people reported that changing the datatype to Object in the interface should solve this issue. Personally, I never tested this issues mainly because this project didn't have any interoperability requirements.

Another interesting aspect to note is that the datasets property is exposed as public and does not follow the regular JavaBean specification (that is, getter and setter for the property). Apparently, the Axis WSDL generator still has some problems to handle arrays of object exposed in a JavaBean. I have fixed this issue exposing the property as public.

At the moment I don't remember exactly which kind of issues I was having exposing the property in a regular way. The Java2wsdl emitter generates a WSDL that basically doesn't work. However, this problem seems to take place only for “input” parameters. I didn't have time to look again into this issue.

The client code is straightforward:

// Create the Transfer Object and populate it

FAMEArgumentsTO to = new FAMEArgumentsTO();

...

try {

// Invoke the webservice...

res = getService(URL).executeFameProcedure(to);

System.err.println("Correlation id: " + res);

} catch (Exception e) {

...

}


The getService method is a private helper method that encapsulates most of the “juicy” code:


private static FameServiceImpl getService(final String endPointUrl) {

FameServiceImpl result = null;


try {

FameServiceImplServiceLocator loc = new FameServiceImplServiceLocator();


if (endPointUrl != null) {

URL url = new URL(endPointUrl);

result = loc.getfameLounge(url);

} else {

result = loc.getfameLounge();

}

} catch (MalformedURLException e) {

...

} catch (ServiceException e) {

...

}


return result;

}
}


This code snippet contains most of the code required to invoke our web service:

  1. Instantiate the serviceServiceLocator (a class generated by Axis during the build process)

  2. Get the service implementation proxy from the locator (loc.getfameLounge)

  3. Return the proxy

  4. Call web service methods against the proxy (.executeFameProcedure(to))

The web service proxy implementation can be invoked passing a URL containing the service endpoint. If the URL is not specified, a default URL will be used (the default URL is the one declared in location property of the the axis-java2wsdlant task). This feature is very handy when it comes to test the web service, because we can deploy the service on different web servers and decide the endpoint location at runtime.


The next method to test is the retrieveResult method. As explained, this method requires a previously acquired correlation ID. Given this ID to the web service, it returns the data related to the ID.

public FAMEExecutionResultsTO retrieveResult(String correlationId);


In this case, the return argument is a POJO, very similar to the previous one:


public class FAMEExecutionResultsTO {

private BinaryFileTO[] binaryFiles;

...

public BinaryFileTO[] getBinaryFiles() {

return binaryFiles;

}

public void setBinaryFiles(BinaryFileTO[] fileTOs) {

binaryFiles = fileTOs;

}

}


This POJO, among other properties, exposes a “binaryFiles” attribute of type BinaryFileTO. Also in this case we have a simple object aggregation. The BinaryFileTO uses the DataHandler object to store the binary information.

The dummy FAMECommandExecutor class reads some test files from the file system, populates the Transfer Object and returns it.


The complete test class is part of the project that comes with this tutorial.

Conclusions

TBD


Copyright 2004

This tutorial is written by Luciano Fiandesio and is licensed under a Creative Commons License.


Please, contact me (lfiandesio@yahoo.com) to reports errors and to add your comments (a wiki version of this document should follow).


Resources